diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/LanguageServerRunner.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/LanguageServerRunner.java index 38c3bc6beb..cba0883570 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/LanguageServerRunner.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/LanguageServerRunner.java @@ -24,6 +24,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.function.Consumer; import java.util.function.Function; import org.eclipse.lsp4j.jsonrpc.Launcher; @@ -38,6 +39,8 @@ import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; import org.springframework.ide.vscode.commons.protocol.STS4LanguageClient; +import com.google.gson.GsonBuilder; + /** * A CommandLineRunner that launches a language server. This meant to be used as a Spring bean * in a SpringBoot app. @@ -98,11 +101,14 @@ public void run(String... args) throws Exception { private Function messageConsumer; - public LanguageServerRunner(LanguageServerProperties properties, SimpleLanguageServer languageServer, Function messageConsumer) { + private Consumer configureGson; + + public LanguageServerRunner(LanguageServerProperties properties, SimpleLanguageServer languageServer, Function messageConsumer, Consumer configureGson) { super(); this.properties = properties; this.languageServer = languageServer; this.messageConsumer = messageConsumer; + this.configureGson = configureGson; } public void start() throws Exception { @@ -207,7 +213,7 @@ private Launcher createSocketLauncher( AsynchronousSocketChannel socketChannel = serverSocket.accept().get(); log.info("Client connected via socket"); return Launcher.createIoLauncher(localService, remoteInterface, Channels.newInputStream(socketChannel), - Channels.newOutputStream(socketChannel), executorService, wrapper); + Channels.newOutputStream(socketChannel), executorService, wrapper, configureGson); } private static Connection connectToNode() throws IOException { @@ -235,12 +241,13 @@ private static Connection connectToNode() throws IOException { private Future runAsync(Connection connection) throws Exception { LanguageServer server = this.languageServer; ExecutorService executor = createServerThreads(); - Launcher launcher = Launcher.createLauncher(server, + Launcher launcher = Launcher.createIoLauncher(server, STS4LanguageClient.class, connection.in, connection.out, executor, - messageConsumer + messageConsumer, + configureGson ); if (server instanceof LanguageClientAware) { diff --git a/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/RuntimeTypeAdapterFactory.java b/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/RuntimeTypeAdapterFactory.java new file mode 100644 index 0000000000..51159d2314 --- /dev/null +++ b/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/RuntimeTypeAdapterFactory.java @@ -0,0 +1,358 @@ +/* + * Copyright (C) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ide.vscode.commons; + +import java.io.IOException; +import java.lang.reflect.AccessFlag; +import java.util.LinkedHashMap; +import java.util.Map; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +/** + * Adapts values whose runtime type may differ from their declaration type. This + * is necessary when a field's type is not the same type that GSON should create + * when deserializing that field. For example, consider these types: + * + *
{@code
+ * abstract class Shape {
+ * 	int x;
+ * 	int y;
+ * }
+ * 
+ * class Circle extends Shape {
+ * 	int radius;
+ * }
+ * 
+ * class Rectangle extends Shape {
+ * 	int width;
+ * 	int height;
+ * }
+ * 
+ * class Diamond extends Shape {
+ * 	int width;
+ * 	int height;
+ * }
+ * 
+ * class Drawing {
+ * 	Shape bottomShape;
+ * 	Shape topShape;
+ * }
+ * }
+ * + *

+ * Without additional type information, the serialized JSON is ambiguous. Is the + * bottom shape in this drawing a rectangle or a diamond? + * + *

{@code
+ * {
+ *   "bottomShape": {
+ *     "width": 10,
+ *     "height": 5,
+ *     "x": 0,
+ *     "y": 0
+ *   },
+ *   "topShape": {
+ *     "radius": 2,
+ *     "x": 4,
+ *     "y": 1
+ *   }
+ * }
+ * }
+ * + * This class addresses this problem by adding type information to the + * serialized JSON and honoring that type information when the JSON is + * deserialized: + * + *
{@code
+ * {
+ *   "bottomShape": {
+ *     "type": "Diamond",
+ *     "width": 10,
+ *     "height": 5,
+ *     "x": 0,
+ *     "y": 0
+ *   },
+ *   "topShape": {
+ *     "type": "Circle",
+ *     "radius": 2,
+ *     "x": 4,
+ *     "y": 1
+ *   }
+ * }
+ * }
+ * + * Both the type field name ({@code "type"}) and the type labels + * ({@code "Rectangle"}) are configurable. + * + *

Registering Types

+ * + * Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type + * field name to the {@link #of} factory method. If you don't supply an explicit + * type field name, {@code "type"} will be used. + * + *
{@code
+ * RuntimeTypeAdapterFactory shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class, "type");
+ * }
+ * + * Next register all of your subtypes. Every subtype must be explicitly + * registered. This protects your application from injection attacks. If you + * don't supply an explicit type label, the type's simple name will be used. + * + *
{@code
+ * shapeAdapterFactory.registerSubtype(Rectangle.class, "Rectangle");
+ * shapeAdapterFactory.registerSubtype(Circle.class, "Circle");
+ * shapeAdapterFactory.registerSubtype(Diamond.class, "Diamond");
+ * }
+ * + * Finally, register the type adapter factory in your application's GSON + * builder: + * + *
{@code
+ * Gson gson = new GsonBuilder().registerTypeAdapterFactory(shapeAdapterFactory).create();
+ * }
+ * + * Like {@code GsonBuilder}, this API supports chaining: + * + *
{@code
+ * RuntimeTypeAdapterFactory shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
+ * 		.registerSubtype(Rectangle.class).registerSubtype(Circle.class).registerSubtype(Diamond.class);
+ * }
+ * + *

Serialization and deserialization

+ * + * In order to serialize and deserialize a polymorphic object, you must specify + * the base type explicitly. + * + *
{@code
+ * Diamond diamond = new Diamond();
+ * String json = gson.toJson(diamond, Shape.class);
+ * }
+ * + * And then: + * + *
{@code
+ * Shape shape = gson.fromJson(json, Shape.class);
+ * }
+ */ +public final class RuntimeTypeAdapterFactory implements TypeAdapterFactory { + private final Class baseType; + private final String typeFieldName; + private final Map> labelToSubtype = new LinkedHashMap<>(); + private final Map, String> subtypeToLabel = new LinkedHashMap<>(); + private final boolean maintainType; + private boolean recognizeSubtypes; + + private RuntimeTypeAdapterFactory(Class baseType, String typeFieldName, boolean maintainType) { + if (typeFieldName == null || baseType == null) { + throw new NullPointerException(); + } + this.baseType = baseType; + this.typeFieldName = typeFieldName; + this.maintainType = maintainType; + } + + /** + * Creates a new runtime type adapter for {@code baseType} using + * {@code typeFieldName} as the type field name. Type field names are case + * sensitive. + * + * @param maintainType true if the type field should be included in deserialized + * objects + */ + public static RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName, boolean maintainType) { + return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, maintainType); + } + + /** + * Creates a new runtime type adapter for {@code baseType} using + * {@code typeFieldName} as the type field name. Type field names are case + * sensitive. + */ + public static RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName) { + return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, false); + } + + /** + * Creates a new runtime type adapter for {@code baseType} using {@code "type"} + * as the type field name. + */ + public static RuntimeTypeAdapterFactory of(Class baseType) { + return new RuntimeTypeAdapterFactory<>(baseType, "type", false); + } + + /** + * Ensures that this factory will handle not just the given {@code baseType}, + * but any subtype of that type. + */ + @CanIgnoreReturnValue + public RuntimeTypeAdapterFactory recognizeSubtypes() { + this.recognizeSubtypes = true; + return this; + } + + /** + * Registers {@code type} identified by {@code label}. Labels are case + * sensitive. + * + * @throws IllegalArgumentException if either {@code type} or {@code label} have + * already been registered on this type + * adapter. + */ + @CanIgnoreReturnValue + public RuntimeTypeAdapterFactory registerSubtype(Class type, String label) { + if (type == null || label == null) { + throw new NullPointerException(); + } + if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) { + throw new IllegalArgumentException("types and labels must be unique"); + } + labelToSubtype.put(label, type); + subtypeToLabel.put(type, label); + return this; + } + + /** + * Registers {@code type} identified by its {@link Class#getSimpleName simple + * name}. Labels are case sensitive. + * + * @throws IllegalArgumentException if either {@code type} or its simple name + * have already been registered on this type + * adapter. + */ + @CanIgnoreReturnValue + public RuntimeTypeAdapterFactory registerSubtype(Class type) { + return registerSubtype(type, type.getSimpleName()); + } + + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + if (type == null) { + return null; + } + Class rawType = type.getRawType(); + boolean handle = recognizeSubtypes ? baseType.isAssignableFrom(rawType) : baseType.equals(rawType); + if (!handle) { + return null; + } + + TypeAdapter jsonElementAdapter = gson.getAdapter(JsonElement.class); + Map> labelToDelegate = new LinkedHashMap<>(); + Map, TypeAdapter> subtypeToDelegate = new LinkedHashMap<>(); + for (Map.Entry> entry : labelToSubtype.entrySet()) { + TypeAdapter delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue())); + labelToDelegate.put(entry.getKey(), delegate); + subtypeToDelegate.put(entry.getValue(), delegate); + } + + final RuntimeTypeAdapterFactory thisAdapter = this; + + return new TypeAdapter() { + + private TypeAdapter findSubtypeDelegate(Class clazz) { + if (clazz.isInterface() || clazz.accessFlags().contains(AccessFlag.ABSTRACT)) { + throw new JsonParseException( + "cannot serialize/deserialize " + clazz.getName() + "; it is abstract"); + } + if (baseType.isAssignableFrom(clazz)) { + TypeAdapter delegate = gson.getDelegateAdapter(thisAdapter, TypeToken.get(clazz)); + labelToDelegate.put(clazz.getName(), delegate); + subtypeToDelegate.put(clazz, delegate); + labelToSubtype.put(clazz.getName(), clazz); + subtypeToLabel.put(clazz, clazz.getName()); + return delegate; + } + return null; + } + + @SuppressWarnings("unchecked") // registration requires that subtype extends T + @Override + public R read(JsonReader in) throws IOException { + JsonElement jsonElement = jsonElementAdapter.read(in); + JsonElement labelJsonElement; + if (maintainType) { + labelJsonElement = jsonElement.getAsJsonObject().get(typeFieldName); + } else { + labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName); + } + + if (labelJsonElement == null) { + throw new JsonParseException("cannot deserialize " + baseType + + " because it does not define a field named " + typeFieldName); + } + String label = labelJsonElement.getAsString(); + TypeAdapter delegate = (TypeAdapter) labelToDelegate.get(label); + if (delegate == null && recognizeSubtypes) { + try { + delegate = (TypeAdapter) findSubtypeDelegate(Class.forName(label)); + } catch (ClassNotFoundException e) { + // ignore + } + } + if (delegate == null) { + throw new JsonParseException("cannot deserialize " + baseType + " subtype named " + label + + "; did you forget to register a subtype?"); + } + return delegate.fromJsonTree(jsonElement); + } + + @SuppressWarnings("unchecked") // registration requires that subtype extends T + @Override + public void write(JsonWriter out, R value) throws IOException { + Class srcType = value.getClass(); + TypeAdapter delegate = (TypeAdapter) subtypeToDelegate.get(srcType); + if (delegate == null && recognizeSubtypes) { + delegate = (TypeAdapter) findSubtypeDelegate(srcType); + } + if (delegate == null) { + throw new JsonParseException( + "cannot serialize " + srcType.getName() + "; did you forget to register a subtype?"); + } + JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject(); + + if (maintainType) { + jsonElementAdapter.write(out, jsonObject); + return; + } + + JsonObject clone = new JsonObject(); + + if (jsonObject.has(typeFieldName)) { + throw new JsonParseException("cannot serialize " + srcType.getName() + + " because it already defines a field named " + typeFieldName); + } + String label = subtypeToLabel.get(srcType); + clone.add(typeFieldName, new JsonPrimitive(label)); + + for (Map.Entry e : jsonObject.entrySet()) { + clone.add(e.getKey(), e.getValue()); + } + jsonElementAdapter.write(out, clone); + } + }.nullSafe(); + } +} \ No newline at end of file diff --git a/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/spring/Bean.java b/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/spring/Bean.java index e3ba120ebe..3bfbdc844f 100644 --- a/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/spring/Bean.java +++ b/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/spring/Bean.java @@ -11,11 +11,13 @@ package org.springframework.ide.vscode.commons.protocol.spring; import java.util.Set; +import java.util.stream.Collectors; import org.eclipse.lsp4j.DocumentSymbol; import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.SymbolKind; +import com.google.common.collect.ImmutableSet; import com.google.gson.Gson; public class Bean extends AbstractSpringIndexElement implements SymbolElement { @@ -28,6 +30,7 @@ public class Bean extends AbstractSpringIndexElement implements SymbolElement { private final AnnotationMetadata[] annotations; private final boolean isConfiguration; private final String symbolLabel; + private final boolean isInterface; public Bean( String name, @@ -44,26 +47,25 @@ public Bean( this.location = location; this.isConfiguration = isConfiguration; this.symbolLabel = symbolLabel; + this.isInterface = supertypes == null || !supertypes.contains(Object.class.getName()); if (injectionPoints != null && injectionPoints.length == 0) { - this.injectionPoints = DefaultValues.EMPTY_INJECTION_POINTS; + this.injectionPoints = null; } else { this.injectionPoints = injectionPoints; } - if (supertypes != null && supertypes.size() == 0) { - this.supertypes = DefaultValues.EMPTY_SUPERTYPES; - } - else if (supertypes != null && supertypes.size() == 1 && supertypes.contains("java.lang.Object")) { - this.supertypes = DefaultValues.OBJECT_SUPERTYPE; + Set sanitizedSuperTypes = supertypes == null ? null : supertypes.stream().filter(t -> !t.equals(Object.class.getName())).collect(Collectors.toUnmodifiableSet()); + if (sanitizedSuperTypes != null && sanitizedSuperTypes.size() == 0) { + this.supertypes = null; } else { - this.supertypes = supertypes; + this.supertypes = sanitizedSuperTypes; } if (annotations != null && annotations.length == 0) { - this.annotations = DefaultValues.EMPTY_ANNOTATIONS; + this.annotations = null; } else { this.annotations = annotations; @@ -83,15 +85,15 @@ public Location getLocation() { } public InjectionPoint[] getInjectionPoints() { - return injectionPoints; + return injectionPoints == null ? DefaultValues.EMPTY_INJECTION_POINTS : injectionPoints; } public boolean isTypeCompatibleWith(String type) { - return type != null && ((this.type != null && this.type.equals(type)) || (supertypes.contains(type))); + return type != null && ((this.type != null && this.type.equals(type)) || (supertypes != null && supertypes.contains(type)) || (Object.class.getName().equals(type) && !isInterface)); } public AnnotationMetadata[] getAnnotations() { - return annotations; + return annotations == null ? DefaultValues.EMPTY_ANNOTATIONS : annotations; } public boolean isConfiguration() { @@ -99,7 +101,11 @@ public boolean isConfiguration() { } public Set getSupertypes() { - return supertypes; + if (supertypes == null) { + return isInterface ? DefaultValues.EMPTY_SUPERTYPES : DefaultValues.OBJECT_SUPERTYPE; + } else { + return isInterface ? supertypes : ImmutableSet.builder().addAll(supertypes).add(Object.class.getName()).build(); + } } public String getSymbolLabel() { diff --git a/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/spring/InjectionPoint.java b/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/spring/InjectionPoint.java index 57598faab8..b3794d0e82 100644 --- a/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/spring/InjectionPoint.java +++ b/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/spring/InjectionPoint.java @@ -29,8 +29,8 @@ public InjectionPoint(String name, String type, Location location, AnnotationMet this.type = type; this.location = location; - if (annotations == null || (annotations != null && annotations.length == 0)) { - this.annotations = DefaultValues.EMPTY_ANNOTATIONS; + if (annotations != null && annotations.length == 0) { + this.annotations = null; } else { this.annotations = annotations; @@ -50,7 +50,7 @@ public Location getLocation() { } public AnnotationMetadata[] getAnnotations() { - return annotations; + return annotations == null ? DefaultValues.EMPTY_ANNOTATIONS : annotations; } } diff --git a/headless-services/commons/language-server-starter/src/main/java/org/springframework/ide/vscode/languageserver/starter/LanguageServerRunnerAutoConf.java b/headless-services/commons/language-server-starter/src/main/java/org/springframework/ide/vscode/languageserver/starter/LanguageServerRunnerAutoConf.java index ba7b206546..3d6f7bcb83 100644 --- a/headless-services/commons/language-server-starter/src/main/java/org/springframework/ide/vscode/languageserver/starter/LanguageServerRunnerAutoConf.java +++ b/headless-services/commons/language-server-starter/src/main/java/org/springframework/ide/vscode/languageserver/starter/LanguageServerRunnerAutoConf.java @@ -10,6 +10,8 @@ *******************************************************************************/ package org.springframework.ide.vscode.languageserver.starter; +import java.util.Optional; +import java.util.function.Consumer; import java.util.function.Function; import org.eclipse.lsp4j.jsonrpc.MessageConsumer; @@ -21,6 +23,8 @@ import org.springframework.ide.vscode.commons.languageserver.util.ParentProcessWatcher; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; +import com.google.gson.GsonBuilder; + @AutoConfiguration public class LanguageServerRunnerAutoConf { @@ -35,12 +39,13 @@ Function messageConsumer(SimpleLanguageServer @ConditionalOnMissingClass("org.springframework.ide.vscode.languageserver.testharness.LanguageServerHarness") @Bean - public LanguageServerRunner serverApp( + LanguageServerRunner serverApp( LanguageServerProperties properties, SimpleLanguageServer languageServerFactory, - Function messageConsumer + Function messageConsumer, + Optional> configureGson ) { - return new LanguageServerRunner(properties, languageServerFactory, messageConsumer); + return new LanguageServerRunner(properties, languageServerFactory, messageConsumer, configureGson.orElse(b -> {})); } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java index ad88033b5b..d281462c2c 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java @@ -21,6 +21,7 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; import java.util.stream.Collectors; import org.eclipse.lsp4j.CodeActionKind; @@ -89,6 +90,7 @@ import org.springframework.ide.vscode.boot.xml.SpringXMLCompletionEngine; import org.springframework.ide.vscode.boot.yaml.completions.ApplicationYamlAssistContext; import org.springframework.ide.vscode.boot.yaml.completions.SpringYamlCompletionEngine; +import org.springframework.ide.vscode.commons.RuntimeTypeAdapterFactory; import org.springframework.ide.vscode.commons.languageserver.LanguageServerRunner; import org.springframework.ide.vscode.commons.languageserver.java.FutureProjectFinder; import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; @@ -98,6 +100,7 @@ import org.springframework.ide.vscode.commons.languageserver.util.LspClient; import org.springframework.ide.vscode.commons.languageserver.util.ServerCapabilityInitializer; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; +import org.springframework.ide.vscode.commons.protocol.spring.SpringIndexElement; import org.springframework.ide.vscode.commons.util.FileObserver; import org.springframework.ide.vscode.commons.util.LogRedirect; import org.springframework.ide.vscode.commons.util.text.IDocument; @@ -118,6 +121,7 @@ import com.google.common.io.Files; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; import reactor.core.publisher.Hooks; @@ -428,4 +432,10 @@ ModulithService modulithService(SimpleLanguageServer server, JavaProjectFinder p return new ResponseModifier(); } + @Bean + Consumer configureGson() { + return builder -> builder + .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(SpringIndexElement.class, "_internal_node_type") + .recognizeSubtypes()); + } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/CommandsConfig.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/CommandsConfig.java index dd84921707..a7a7390ce2 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/CommandsConfig.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/CommandsConfig.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2024 Broadcom, Inc. + * Copyright (c) 2024, 2025 Broadcom, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -12,6 +12,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; +import org.springframework.ide.vscode.boot.java.commands.SpringIndexCommands; import org.springframework.ide.vscode.boot.java.commands.WorkspaceBootExecutableProjects; import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; @@ -22,5 +24,10 @@ public class CommandsConfig { @Bean WorkspaceBootExecutableProjects workspaceBootProjects(SimpleLanguageServer server, JavaProjectFinder projectFinder, SpringSymbolIndex symbolIndex) { return new WorkspaceBootExecutableProjects(server, projectFinder, symbolIndex); } + + @Bean SpringIndexCommands springIndexCommands(SimpleLanguageServer server, JavaProjectFinder projectFinder, SpringMetamodelIndex symbolIndex) { + return new SpringIndexCommands(server, symbolIndex, projectFinder); + } + } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/index/cache/IndexCacheOnDiscDeltaBased.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/index/cache/IndexCacheOnDiscDeltaBased.java index 503b7466bf..4334391455 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/index/cache/IndexCacheOnDiscDeltaBased.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/index/cache/IndexCacheOnDiscDeltaBased.java @@ -38,13 +38,9 @@ import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang3.tuple.Pair; -import org.eclipse.lsp4j.Location; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.ide.vscode.commons.protocol.spring.AnnotationMetadata; -import org.springframework.ide.vscode.commons.protocol.spring.Bean; -import org.springframework.ide.vscode.commons.protocol.spring.DefaultValues; -import org.springframework.ide.vscode.commons.protocol.spring.InjectionPoint; +import org.springframework.ide.vscode.commons.RuntimeTypeAdapterFactory; import org.springframework.ide.vscode.commons.protocol.spring.SpringIndexElement; import org.springframework.ide.vscode.commons.util.UriUtil; @@ -569,10 +565,9 @@ public IndexCacheStore apply(IndexCacheStore store) { public static Gson createGson() { return new GsonBuilder() .registerTypeAdapter(DeltaStorage.class, new DeltaStorageAdapter()) - .registerTypeAdapter(Bean.class, new BeanJsonAdapter()) - .registerTypeAdapter(InjectionPoint.class, new InjectionPointJsonAdapter()) .registerTypeAdapter(IndexCacheStore.class, new IndexCacheStoreAdapter()) - .registerTypeAdapter(SpringIndexElement.class, new SpringIndexElementAdapter()) + .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(SpringIndexElement.class, "_internal_node_type") + .recognizeSubtypes()) .create(); } @@ -635,111 +630,5 @@ public DeltaStorage deserialize(JsonElement json, Type type, JsonDeserializat } } - private static class BeanJsonAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public Bean deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException { - JsonObject parsedObject = json.getAsJsonObject(); - - String beanName = parsedObject.get("name").getAsString(); - String beanType = parsedObject.get("type").getAsString(); - - JsonElement locationObject = parsedObject.get("location"); - Location location = context.deserialize(locationObject, Location.class); - - JsonElement injectionPointObject = parsedObject.get("injectionPoints"); - InjectionPoint[] injectionPoints = context.deserialize(injectionPointObject, InjectionPoint[].class); - - JsonElement supertypesObject = parsedObject.get("supertypes"); - Set supertypes = context.deserialize(supertypesObject, Set.class); - - JsonElement annotationsObject = parsedObject.get("annotations"); - AnnotationMetadata[] annotations = annotationsObject == null ? DefaultValues.EMPTY_ANNOTATIONS : context.deserialize(annotationsObject, AnnotationMetadata[].class); - - JsonElement isConfigurationObject = parsedObject.get("isConfiguration"); - boolean isConfiguration = context.deserialize(isConfigurationObject, boolean.class); - - String symbolLabel = parsedObject.get("symbolLabel").getAsString(); - - JsonElement childrenObject = parsedObject.get("children"); - Type childrenListType = TypeToken.getParameterized(List.class, SpringIndexElement.class).getType(); - List children = context.deserialize(childrenObject, childrenListType); - - Bean bean = new Bean(beanName, beanType, location, injectionPoints, supertypes, annotations, isConfiguration, symbolLabel); - - for (SpringIndexElement springIndexElement : children) { - bean.addChild(springIndexElement); - } - - return bean; - } - - @Override - public JsonElement serialize(Bean src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject bean = new JsonObject(); - - bean.addProperty("name", src.getName()); - bean.addProperty("type", src.getType()); - - bean.add("location", context.serialize(src.getLocation())); - bean.add("injectionPoints", context.serialize(src.getInjectionPoints())); - - bean.add("supertypes", context.serialize(src.getSupertypes())); - bean.add("annotations", context.serialize(src.getAnnotations())); - - bean.addProperty("isConfiguration", src.isConfiguration()); - bean.addProperty("symbolLabel", src.getSymbolLabel()); - - Type childrenListType = TypeToken.getParameterized(List.class, SpringIndexElement.class).getType(); - bean.add("children", context.serialize(src.getChildren(), childrenListType)); - - bean.addProperty("_internal_node_type", src.getClass().getName()); - - return bean; - } - - } - - private static class InjectionPointJsonAdapter implements JsonDeserializer { - - @Override - public InjectionPoint deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException { - JsonObject parsedObject = json.getAsJsonObject(); - - String injectionPointName = parsedObject.get("name").getAsString(); - String injectionPointType = parsedObject.get("type").getAsString(); - - JsonElement locationObject = parsedObject.get("location"); - Location location = context.deserialize(locationObject, Location.class); - - JsonElement annotationsObject = parsedObject.get("annotations"); - AnnotationMetadata[] annotations = annotationsObject == null ? DefaultValues.EMPTY_ANNOTATIONS : context.deserialize(annotationsObject, AnnotationMetadata[].class); - - return new InjectionPoint(injectionPointName, injectionPointType, location, annotations); - } - } - - private static class SpringIndexElementAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(SpringIndexElement element, Type typeOfSrc, JsonSerializationContext context) { - JsonElement elem = context.serialize(element); - elem.getAsJsonObject().addProperty("_internal_node_type", element.getClass().getName()); - return elem; - } - - @Override - public SpringIndexElement deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException { - JsonObject jsonObject = json.getAsJsonObject(); - String typeName = jsonObject.get("_internal_node_type").getAsString(); - - try { - return context.deserialize(jsonObject, (Class) Class.forName(typeName)); - } catch (ClassNotFoundException e) { - throw new JsonParseException(e); - } - } - } - } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/SpringIndexCommands.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/SpringIndexCommands.java new file mode 100644 index 0000000000..1a2dd7d851 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/SpringIndexCommands.java @@ -0,0 +1,15 @@ +package org.springframework.ide.vscode.boot.java.commands; + +import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; +import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; +import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; + +public class SpringIndexCommands { + + private static final String SPRING_STRUCTURE_CMD = "sts/spring-boot/structure"; + + public SpringIndexCommands(SimpleLanguageServer server, SpringMetamodelIndex metamodelIndex, JavaProjectFinder projectFinder) { + server.onCommand(SPRING_STRUCTURE_CMD, params -> server.getAsync().invoke(() -> metamodelIndex.getProjects())); + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/JdtCronVisitorUtils.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/JdtCronVisitorUtils.java index f39969eeba..1095499b62 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/JdtCronVisitorUtils.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/JdtCronVisitorUtils.java @@ -53,10 +53,12 @@ private static boolean isCronExpression(Expression e) { } else if (e instanceof TextBlock tb) { value = tb.getLiteralValue(); } - value = value.trim(); - if (value.startsWith("#{") || value.startsWith("${")) { - // Either SPEL or Property Holder - return false; + if (value != null) { + value = value.trim(); + if (value.startsWith("#{") || value.startsWith("${")) { + // Either SPEL or Property Holder + return false; + } } return value != null; } diff --git a/vscode-extensions/vscode-spring-boot/lib/Main.ts b/vscode-extensions/vscode-spring-boot/lib/Main.ts index c5ae4278d9..85ed3b9add 100644 --- a/vscode-extensions/vscode-spring-boot/lib/Main.ts +++ b/vscode-extensions/vscode-spring-boot/lib/Main.ts @@ -27,6 +27,8 @@ import * as springBootAgent from './copilot/springBootAgent'; import { applyLspEdit } from "./copilot/guideApply"; import { isLlmApiReady } from "./copilot/util"; import CopilotRequest, { logger } from "./copilot/copilotRequest"; +import { ExplorerTreeProvider } from "./explorer/explorer-tree-provider"; +import { StructureManager } from "./explorer/structure-tree-manager"; const PROPERTIES_LANGUAGE_ID = "spring-boot-properties"; const YAML_LANGUAGE_ID = "spring-boot-properties-yaml"; @@ -52,8 +54,8 @@ export function activate(context: ExtensionContext): Thenable { ], checkjvm: (context: ExtensionContext, jvm: commons.JVM) => { let version = jvm.getMajorVersion(); - if (version < 17) { - throw Error(`Spring Tools Language Server requires Java 17 or higher to be launched. Current Java version is ${version}`); + if (version < 21) { + throw Error(`Spring Tools Language Server requires Java 21 or higher to be launched. Current Java version is ${version}`); } if (!jvm.isJdk()) { @@ -149,7 +151,39 @@ export function activate(context: ExtensionContext): Thenable { context.subscriptions.push(startDebugSupport()); return commons.activate(options, context).then(client => { - commands.registerCommand('vscode-spring-boot.ls.start', () => client.start().then(() => { + + // Spring structure tree in the Explorer view + /* + Requires the following code to be added in the `package.json` to + 1. Declare view: + "views": { + "explorer": [ + { + "id": "explorer.spring", + "name": "Spring", + "when": "java:serverMode || workbenchState==empty", + "contextualTitle": "Spring", + "icon": "resources/logo.png" + } + ] + }, + + 2. Menu item (toolbar action) on the explorer view delegating to the command + "view/title": [ + { + "command": "vscode-spring-boot.structure.refresh", + "when": "view == explorer.spring", + "group": "navigation@5" + } + ], + + */ + // const structureManager = new StructureManager(); + // const explorerTreeProvider = new ExplorerTreeProvider(structureManager); + // context.subscriptions.push(window.createTreeView('explorer.spring', { treeDataProvider: explorerTreeProvider, showCollapseAll: true })); + // context.subscriptions.push(commands.registerCommand("vscode-spring-boot.structure.refresh", () => structureManager.refresh())); + + context.subscriptions.push(commands.registerCommand('vscode-spring-boot.ls.start', () => client.start().then(() => { // Boot LS is fully started registerClasspathService(client); registerJavaDataService(client); @@ -162,8 +196,8 @@ export function activate(context: ExtensionContext): Thenable { // Register TestJars launch support context.subscriptions.push(startTestJarSupport()); - })); - commands.registerCommand('vscode-spring-boot.ls.stop', () => client.stop()); + }))); + context.subscriptions.push(commands.registerCommand('vscode-spring-boot.ls.stop', () => client.stop())); liveHoverUi.activate(client, options, context); rewrite.activate(client, options, context); setLogLevelUi.activate(client, options, context); @@ -175,9 +209,13 @@ export function activate(context: ExtensionContext): Thenable { registerMiscCommands(context); - commands.registerCommand('vscode-spring-boot.agent.apply', applyLspEdit); + context.subscriptions.push(commands.registerCommand('vscode-spring-boot.agent.apply', applyLspEdit)); + + const api = new ApiManager(client).api - return new ApiManager(client).api; + // context.subscriptions.push(api.getSpringIndex().onSpringIndexUpdated(e => structureManager.refresh())); + + return api; }); } diff --git a/vscode-extensions/vscode-spring-boot/lib/api.d.ts b/vscode-extensions/vscode-spring-boot/lib/api.d.ts index 863a6d99e5..13bea525d8 100644 --- a/vscode-extensions/vscode-spring-boot/lib/api.d.ts +++ b/vscode-extensions/vscode-spring-boot/lib/api.d.ts @@ -1,4 +1,4 @@ -import { Event } from "vscode"; +import { Event, Uri } from "vscode"; import { LanguageClient } from "vscode-languageclient/node"; import { LiveProcess } from "./notification"; import {Location} from "vscode-languageclient"; diff --git a/vscode-extensions/vscode-spring-boot/lib/apiManager.ts b/vscode-extensions/vscode-spring-boot/lib/apiManager.ts index 8462c75997..96009dd66b 100644 --- a/vscode-extensions/vscode-spring-boot/lib/apiManager.ts +++ b/vscode-extensions/vscode-spring-boot/lib/apiManager.ts @@ -1,6 +1,6 @@ -import { commands, Uri } from "vscode"; +import { commands } from "vscode"; import { Emitter, LanguageClient } from "vscode-languageclient/node"; -import {Bean, BeansParams, ExtensionAPI, SpringIndex} from "./api"; +import {Bean, BeansParams, ExtensionAPI} from "./api"; import { LiveProcess, LiveProcessConnectedNotification, diff --git a/vscode-extensions/vscode-spring-boot/lib/explorer/explorer-tree-provider.ts b/vscode-extensions/vscode-spring-boot/lib/explorer/explorer-tree-provider.ts new file mode 100644 index 0000000000..0d6e481c86 --- /dev/null +++ b/vscode-extensions/vscode-spring-boot/lib/explorer/explorer-tree-provider.ts @@ -0,0 +1,40 @@ +import { CancellationToken, commands, Event, EventEmitter, ProviderResult, TreeDataProvider, TreeItem, TreeItemCollapsibleState } from "vscode"; +import { StructureManager } from "./structure-tree-manager"; +import { DocumentNode, ProjectNode, SpringNode } from "./nodes"; +import * as Path from "path"; + +export class ExplorerTreeProvider implements TreeDataProvider { + + private emitter: EventEmitter; + public readonly onDidChangeTreeData: Event; + + constructor(private manager: StructureManager) { + this.emitter = new EventEmitter(); + this.onDidChangeTreeData = this.emitter.event; + this.manager.onDidChange(e => this.emitter.fire(e)); + } + + getTreeItem(element: SpringNode): TreeItem | Thenable { + return element.getTreeItem(); + } + + getChildren(element?: SpringNode): ProviderResult { + if (element) { + return element.children; + } + return this.getRootElements(); + } + + getRootElements(): ProviderResult { + return this.manager.rootElements; + } + + // getParent?(element: SpringNode): ProviderResult { + // throw new Error("Method not implemented."); + // } + + // resolveTreeItem?(item: TreeItem, element: SpringNode, token: CancellationToken): ProviderResult { + // throw new Error("Method not implemented."); + // } + +} \ No newline at end of file diff --git a/vscode-extensions/vscode-spring-boot/lib/explorer/nodes.ts b/vscode-extensions/vscode-spring-boot/lib/explorer/nodes.ts new file mode 100644 index 0000000000..e14943850c --- /dev/null +++ b/vscode-extensions/vscode-spring-boot/lib/explorer/nodes.ts @@ -0,0 +1,222 @@ +import { ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri } from "vscode"; +import { Location, Range } from "vscode-languageclient"; + +export class SpringNode { + constructor(readonly children: SpringNode[]) {} + getTreeItem(): TreeItem { + return new TreeItem("", this.computeState(TreeItemCollapsibleState.Expanded)); + } + computeState(defaultState: TreeItemCollapsibleState.Collapsed | TreeItemCollapsibleState.Expanded): TreeItemCollapsibleState { + return Array.isArray(this.children) && this.children.length ? defaultState : TreeItemCollapsibleState.None; + } +} + +export class ProjectNode extends SpringNode { + constructor(readonly name: string, children: SpringNode[]) { + super(children); + } + getTreeItem(): TreeItem { + const item = super.getTreeItem(); + item.label = this.name; + return item; + } +} + +export class DocumentNode extends SpringNode { + constructor(readonly docURI: Uri, children: SpringNode[]) { + super(children); + } + getTreeItem(): TreeItem { + const item = super.getTreeItem(); + item.label = undefined; // let VSCode derive the label from the resource URI + item.resourceUri = this.docURI; + item.iconPath = ThemeIcon.File; + return item; + } +} + +export class AotProcessorNode extends SpringNode { + constructor( + children: SpringNode[], + readonly type: string, + readonly docUri: Uri + ) { + super(children); + } + getTreeItem(): TreeItem { + const item = super.getTreeItem(); + item.label = this.type; + item.resourceUri = this.docUri; + return item; + } +} + +export class BeanMethodContainerNode extends SpringNode { + constructor( + children: SpringNode[], + readonly type: string, + readonly location: Location, + ) { + super(children); + } + getTreeItem(): TreeItem { + const item = super.getTreeItem(); + item.label = this.type; + return item; + } +} + +export class BeanRegistrarNode extends SpringNode { + constructor( + children: SpringNode[], + readonly name: string, + readonly type: string, + readonly location: Location, + ) { + super(children); + } + getTreeItem(): TreeItem { + const item = super.getTreeItem(); + item.label = this.name; + return item; + } +} + +export class ConfigPropertyNode extends SpringNode { + constructor( + children: SpringNode[], + readonly name: string, + readonly type: string, + readonly range: Range + ) { + super(children); + } + getTreeItem(): TreeItem { + const item = super.getTreeItem(); + item.label = this.name; + return item; + } +} + +export class EventListenerNode extends SpringNode { + constructor( + children: SpringNode[], + readonly eventType: string, + readonly location: Location, + readonly containerBeanType: string, + readonly annotations: AnnotationMetadata[] + ) { + super(children); + } + getTreeItem(): TreeItem { + const item = super.getTreeItem(); + item.label = this.eventType; + return item; + } +} + +export class EventPublisherNode extends SpringNode { + constructor( + children: SpringNode[], + readonly eventType: string, + readonly location: Location, + readonly eventTypesFromHierarchy: string[] + ) { + super(children); + } + getTreeItem(): TreeItem { + const item = super.getTreeItem(); + item.label = this.eventType; + return item; + } +} + +export class QueryMethodNode extends SpringNode { + constructor( + children: SpringNode[], + readonly methodName: string, + readonly queryString: string, + readonly range: Range + ) { + super(children); + } + getTreeItem(): TreeItem { + const item = super.getTreeItem(); + item.label = this.methodName; + return item; + } +} + +export class RequestMappingNode extends SpringNode { + constructor( + children: SpringNode[], + readonly path: string, + readonly httpMethods: string[], + readonly contentTypes: string[], + readonly acceptTypes: string[], + readonly symbolLabel: string, + readonly range: Range + ) { + super(children); + } + getTreeItem(): TreeItem { + const item = super.getTreeItem(); + item.label = this.path; + return item; + } +} + +export class WebfluxRoutesNode extends RequestMappingNode { + constructor( + children: SpringNode[], + path: string, + httpMethods: string[], + contentTypes: string[], + acceptTypes: string[], + symbolLabel: string, + range: Range, + readonly ranges: Range[] + ) { + super(children, path, httpMethods, contentTypes, acceptTypes, symbolLabel, range); + } +} + +export class BeanNode extends SpringNode { + constructor( + children: SpringNode[], + readonly name: string, + readonly type: string, + readonly location: Location, + readonly injectionPoints: InjectionPoint[], + readonly supertypes: string[], + readonly annotations: AnnotationMetadata[], + readonly isConfiguration: boolean, + readonly symbolLabel: string + ) { + super(children); + } + getTreeItem(): TreeItem { + const item = super.getTreeItem(); + item.label = this.name; + return item; + } +} + +export interface InjectionPoint { + readonly name: string; + readonly type: string; + readonly location: Location; + readonly annotations: AnnotationMetadata[]; +} + +export interface AnnotationMetadata { + readonly annotationType: string; + readonly isMetaAnnotation: boolean; + readonly location: Location; + readonly attributes: {[key: string]: AnnotationAttributeValue[]}; +} + +export interface AnnotationAttributeValue { + readonly name: string; + readonly location: Location; +} diff --git a/vscode-extensions/vscode-spring-boot/lib/explorer/structure-tree-manager.ts b/vscode-extensions/vscode-spring-boot/lib/explorer/structure-tree-manager.ts new file mode 100644 index 0000000000..2348c9d0b6 --- /dev/null +++ b/vscode-extensions/vscode-spring-boot/lib/explorer/structure-tree-manager.ts @@ -0,0 +1,124 @@ +import { commands, EventEmitter, Event, Uri } from "vscode"; +import { AotProcessorNode, BeanMethodContainerNode, BeanNode, BeanRegistrarNode, ConfigPropertyNode, DocumentNode, EventListenerNode, EventPublisherNode, ProjectNode, QueryMethodNode, RequestMappingNode, SpringNode, WebfluxRoutesNode } from "./nodes"; + +const SPRING_STRUCTURE_CMD = "sts/spring-boot/structure"; + +export class StructureManager { + + private _rootElements: Thenable + private _onDidChange: EventEmitter = new EventEmitter(); + + get rootElements(): Thenable { + return this._rootElements; + } + + refresh(): void { + this._rootElements = commands.executeCommand(SPRING_STRUCTURE_CMD).then(json => { + const nodes = this.parseArray(json); + this._onDidChange.fire(undefined); + return nodes; + }); + } + + private parseNode(json: any): SpringNode | undefined { + if (typeof (json._internal_node_type) === 'string') { + switch (json._internal_node_type) { + case "org.springframework.ide.vscode.commons.protocol.spring.ProjectElement": + return new ProjectNode(json.projectName as string, this.parseArray(json.children)); + case "org.springframework.ide.vscode.commons.protocol.spring.DocumentElement": + return new DocumentNode(Uri.parse(json.docURI as string), this.parseArray(json.children)); + case "org.springframework.ide.vscode.commons.protocol.spring.Bean": + return new BeanNode( + this.parseArray(json.children), + json.name, + json.type, + json.location, + json.injectionPoints, + json.supertypes, + json.annotations, + json.isConfiguration, + json.symbolLabel + ); + case "org.springframework.ide.vscode.commons.protocol.spring.AotProcessorElement": + return new AotProcessorNode( + this.parseArray(json.children), + json.name, + Uri.parse(json.docUri) + ); + case "org.springframework.ide.vscode.commons.protocol.spring.BeanMethodContainerElement": + return new BeanMethodContainerNode( + this.parseArray(json.children), + json.type, + json.location + ); + case "org.springframework.ide.vscode.commons.protocol.spring.BeanRegistrarElement": + return new BeanRegistrarNode( + this.parseArray(json.children), + json.name, + json.type, + json.location + + ); + case "org.springframework.ide.vscode.boot.java.beans.ConfigPropertyIndexElement": + return new ConfigPropertyNode( + this.parseArray(json.children), + json.name, + json.type, + json.range + ); + case "org.springframework.ide.vscode.boot.java.events.EventListenerIndexElement": + return new EventListenerNode( + this.parseArray(json.children), + json.eventType, + json.location, + json.containerBeanType, + json.annotations + ); + case "org.springframework.ide.vscode.boot.java.events.EventPublisherIndexElement": + return new EventPublisherNode( + this.parseArray(json.children), + json.eventType, + json.location, + json.eventTypesFromHierarchy, + ); + case "org.springframework.ide.vscode.boot.java.data.QueryMethodIndexElement": + return new QueryMethodNode( + this.parseArray(json.children), + json.methodName, + json.queryString, + json.range + ); + case "org.springframework.ide.vscode.boot.java.requestmapping.RequestMappingIndexElement": + return new RequestMappingNode( + this.parseArray(json.children), + json.path, + json.httpMethods, + json.contentTypes, + json.acceptTypes, + json.symbolLabel, + json.range + ); + case "org.springframework.ide.vscode.boot.java.requestmapping.WebfluxRouteElementRangesIndexElement": + return new WebfluxRoutesNode( + this.parseArray(json.children), + json.path, + json.httpMethods, + json.contentTypes, + json.acceptTypes, + json.symbolLabel, + json.range, + json.ranges + ); + } + } + } + + private parseArray(json: any): SpringNode[] { + return Array.isArray(json) ? (json as []).map(j => this.parseNode(j)).filter(e => !!e) : []; + } + + public get onDidChange(): Event { + return this._onDidChange.event; + } + +} \ No newline at end of file diff --git a/vscode-extensions/vscode-spring-boot/package.json b/vscode-extensions/vscode-spring-boot/package.json index cf331fb63c..0c3233b693 100644 --- a/vscode-extensions/vscode-spring-boot/package.json +++ b/vscode-extensions/vscode-spring-boot/package.json @@ -144,6 +144,10 @@ } ], "commandPalette": [ + { + "command": "vscode-spring-boot.structure.refresh", + "when": "false" + }, { "command": "vscode-spring-boot.props-to-yaml", "when": "false" @@ -184,6 +188,12 @@ ] }, "commands": [ + { + "command": "vscode-spring-boot.structure.refresh", + "title": "Refresh", + "category": "Spring Boot", + "icon": "$(refresh)" + }, { "command": "vscode-spring-boot.live-hover.connect", "title": "Show/Refresh/Hide Live Data from Spring Boot Processes",