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 38c3bc6be..cba088357 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.nio.channels.Channels; 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.config.LanguageServ 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 class LanguageServerRunner implements CommandLineRunner { 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 @@ public class LanguageServerRunner implements CommandLineRunner { 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 @@ public class LanguageServerRunner implements CommandLineRunner { 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 000000000..8d67f658e --- /dev/null +++ b/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/RuntimeTypeAdapterFactory.java @@ -0,0 +1,331 @@ +/* + * 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 + * + * http://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 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; +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 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); + } + + return new TypeAdapter() { + @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(); + @SuppressWarnings("unchecked") // registration requires that subtype extends T + TypeAdapter delegate = (TypeAdapter) labelToDelegate.get(label); + if (delegate == null) { + throw new JsonParseException( + "cannot deserialize " + + baseType + + " subtype named " + + label + + "; did you forget to register a subtype?"); + } + return delegate.fromJsonTree(jsonElement); + } + + @Override + public void write(JsonWriter out, R value) throws IOException { + Class srcType = value.getClass(); + String label = subtypeToLabel.get(srcType); + @SuppressWarnings("unchecked") // registration requires that subtype extends T + TypeAdapter delegate = (TypeAdapter) subtypeToDelegate.get(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); + } + 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/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 ba7b20654..3d6f7bcb8 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.config.LanguageServ 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 @@ public class LanguageServerRunnerAutoConf { @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 ad88033b5..eb083d24f 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.List; 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; @@ -45,6 +46,7 @@ import org.springframework.ide.vscode.boot.index.cache.IndexCache; import org.springframework.ide.vscode.boot.index.cache.IndexCacheOnDiscDeltaBased; import org.springframework.ide.vscode.boot.index.cache.IndexCacheVoid; import org.springframework.ide.vscode.boot.java.JavaDefinitionHandler; +import org.springframework.ide.vscode.boot.java.beans.ConfigPropertyIndexElement; import org.springframework.ide.vscode.boot.java.beans.DependsOnDefinitionProvider; import org.springframework.ide.vscode.boot.java.beans.NamedDefinitionProvider; import org.springframework.ide.vscode.boot.java.beans.QualifierDefinitionProvider; @@ -52,8 +54,11 @@ import org.springframework.ide.vscode.boot.java.beans.ResourceDefinitionProvider import org.springframework.ide.vscode.boot.java.conditionals.ConditionalOnBeanDefinitionProvider; import org.springframework.ide.vscode.boot.java.conditionals.ConditionalOnResourceDefinitionProvider; import org.springframework.ide.vscode.boot.java.copilot.util.ResponseModifier; +import org.springframework.ide.vscode.boot.java.data.QueryMethodIndexElement; import org.springframework.ide.vscode.boot.java.data.jpa.queries.DataQueryParameterDefinitionProvider; import org.springframework.ide.vscode.boot.java.data.jpa.queries.JdtDataQuerySemanticTokensProvider; +import org.springframework.ide.vscode.boot.java.events.EventListenerIndexElement; +import org.springframework.ide.vscode.boot.java.events.EventPublisherIndexElement; import org.springframework.ide.vscode.boot.java.handlers.BootJavaCodeActionProvider; import org.springframework.ide.vscode.boot.java.handlers.BootJavaReconcileEngine; import org.springframework.ide.vscode.boot.java.handlers.JavaCodeActionHandler; @@ -73,6 +78,8 @@ import org.springframework.ide.vscode.boot.java.livehover.v2.SpringProcessLiveDa import org.springframework.ide.vscode.boot.java.reconcilers.JavaReconciler; import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler; import org.springframework.ide.vscode.boot.java.reconcilers.JdtReconciler; +import org.springframework.ide.vscode.boot.java.requestmapping.RequestMappingIndexElement; +import org.springframework.ide.vscode.boot.java.requestmapping.WebfluxRouteElementRangesIndexElement; import org.springframework.ide.vscode.boot.java.spel.SpelDefinitionProvider; import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; import org.springframework.ide.vscode.boot.java.value.ValueDefinitionProvider; @@ -89,6 +96,7 @@ import org.springframework.ide.vscode.boot.properties.completions.SpringProperti 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 +106,12 @@ import org.springframework.ide.vscode.commons.languageserver.util.LanguageComput 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.AbstractSpringIndexElement; +import org.springframework.ide.vscode.commons.protocol.spring.AotProcessorElement; +import org.springframework.ide.vscode.commons.protocol.spring.BeanMethodContainerElement; +import org.springframework.ide.vscode.commons.protocol.spring.DocumentElement; +import org.springframework.ide.vscode.commons.protocol.spring.ProjectElement; +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 +132,7 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; 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 +443,22 @@ public class BootLanguageServerBootApp { return new ResponseModifier(); } + @Bean + Consumer configureGson() { + return builder -> builder + .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(SpringIndexElement.class, "_internal_node_type") + .recognizeSubtypes() + .registerSubtype(org.springframework.ide.vscode.commons.protocol.spring.Bean.class) + .registerSubtype(AotProcessorElement.class) + .registerSubtype(BeanMethodContainerElement.class) + .registerSubtype(ConfigPropertyIndexElement.class) + .registerSubtype(DocumentElement.class) + .registerSubtype(EventListenerIndexElement.class) + .registerSubtype(EventPublisherIndexElement.class) + .registerSubtype(ProjectElement.class) + .registerSubtype(QueryMethodIndexElement.class) + .registerSubtype(RequestMappingIndexElement.class) + .registerSubtype(WebfluxRouteElementRangesIndexElement.class) + .registerSubtype(AbstractSpringIndexElement.class)); + } } 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 503b7466b..8da2a2aaa 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,21 @@ import java.util.stream.Collectors; 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.boot.java.beans.ConfigPropertyIndexElement; +import org.springframework.ide.vscode.boot.java.data.QueryMethodIndexElement; +import org.springframework.ide.vscode.boot.java.events.EventListenerIndexElement; +import org.springframework.ide.vscode.boot.java.events.EventPublisherIndexElement; +import org.springframework.ide.vscode.boot.java.requestmapping.RequestMappingIndexElement; +import org.springframework.ide.vscode.boot.java.requestmapping.WebfluxRouteElementRangesIndexElement; +import org.springframework.ide.vscode.commons.RuntimeTypeAdapterFactory; +import org.springframework.ide.vscode.commons.protocol.spring.AbstractSpringIndexElement; +import org.springframework.ide.vscode.commons.protocol.spring.AotProcessorElement; 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.protocol.spring.BeanMethodContainerElement; +import org.springframework.ide.vscode.commons.protocol.spring.DocumentElement; +import org.springframework.ide.vscode.commons.protocol.spring.ProjectElement; import org.springframework.ide.vscode.commons.protocol.spring.SpringIndexElement; import org.springframework.ide.vscode.commons.util.UriUtil; @@ -569,10 +577,23 @@ public class IndexCacheOnDiscDeltaBased implements IndexCache { 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() + .registerSubtype(Bean.class) + .registerSubtype(AotProcessorElement.class) + .registerSubtype(BeanMethodContainerElement.class) + .registerSubtype(ConfigPropertyIndexElement.class) + .registerSubtype(DocumentElement.class) + .registerSubtype(EventListenerIndexElement.class) + .registerSubtype(EventPublisherIndexElement.class) + .registerSubtype(ProjectElement.class) + .registerSubtype(QueryMethodIndexElement.class) + .registerSubtype(RequestMappingIndexElement.class) + .registerSubtype(WebfluxRouteElementRangesIndexElement.class) + .registerSubtype(AbstractSpringIndexElement.class)) + + .create(); } @@ -635,111 +656,5 @@ public class IndexCacheOnDiscDeltaBased implements IndexCache { } } - 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 index 409e223de..1a2dd7d85 100644 --- 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 @@ -1,27 +1,15 @@ package org.springframework.ide.vscode.boot.java.commands; -import java.util.List; -import java.util.concurrent.CompletableFuture; - -import org.eclipse.lsp4j.TextDocumentIdentifier; import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; -import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; -import com.google.gson.JsonElement; - public class SpringIndexCommands { - private static final String PROJECT_BEANS_CMD = "sts/spring-boot/beans"; + private static final String SPRING_STRUCTURE_CMD = "sts/spring-boot/structure"; public SpringIndexCommands(SimpleLanguageServer server, SpringMetamodelIndex metamodelIndex, JavaProjectFinder projectFinder) { - server.onCommand(PROJECT_BEANS_CMD, params -> { - String projectUri = ((JsonElement) params.getArguments().get(0)).getAsString(); - IJavaProject project = projectFinder.find(new TextDocumentIdentifier(projectUri)).orElse(null); - return project == null ? CompletableFuture.completedFuture(List.of()) - : server.getAsync().execute(() -> metamodelIndex.getBeansOfProject(project.getElementName())); - }); + 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 f39969eeb..1095499b6 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 @@ public class JdtCronVisitorUtils { } 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/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/index/test/SpringMetamodelIndexTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/index/test/SpringMetamodelIndexTest.java index 26c37b6ec..ae3740a8b 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/index/test/SpringMetamodelIndexTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/index/test/SpringMetamodelIndexTest.java @@ -29,12 +29,14 @@ import org.eclipse.lsp4j.Range; import org.junit.jupiter.api.Test; import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; import org.springframework.ide.vscode.boot.index.cache.IndexCacheOnDiscDeltaBased; -import org.springframework.ide.vscode.commons.protocol.spring.AbstractSpringIndexElement; +import org.springframework.ide.vscode.boot.java.beans.ConfigPropertyIndexElement; +import org.springframework.ide.vscode.boot.java.data.QueryMethodIndexElement; import org.springframework.ide.vscode.commons.protocol.spring.AnnotationAttributeValue; 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.protocol.spring.ProjectElement; import org.springframework.ide.vscode.commons.protocol.spring.SpringIndexElement; import com.google.gson.Gson; @@ -269,7 +271,7 @@ public class SpringMetamodelIndexTest { assertEquals("point2", points[1].getName()); assertEquals("point2-type", points[1].getType()); assertEquals(locationForDoc1, points[1].getLocation()); - assertSame(DefaultValues.EMPTY_ANNOTATIONS, points[1].getAnnotations()); + assertEquals(0, points[1].getAnnotations().length); assertTrue(deserializedBean.isTypeCompatibleWith("supertype1")); assertTrue(deserializedBean.isTypeCompatibleWith("supertype2")); @@ -309,7 +311,7 @@ public class SpringMetamodelIndexTest { assertEquals("beanType", deserializedBean.getType()); assertEquals(locationForDoc1, deserializedBean.getLocation()); - assertSame(DefaultValues.EMPTY_INJECTION_POINTS, deserializedBean.getInjectionPoints()); + assertEquals(0, deserializedBean.getInjectionPoints().length); } @Test @@ -401,7 +403,7 @@ public class SpringMetamodelIndexTest { void testBasicSpringIndexStructure() { Bean bean1 = new Bean("beanName1", "beanType1", locationForDoc1, emptyInjectionPoints, Set.of("supertype1", "supertype2"), emptyAnnotations, false, "symbolLabel"); - SubType1 child1 = new SubType1(); + ConfigPropertyIndexElement child1 = new ConfigPropertyIndexElement("prop1", "java.lang.String", null); bean1.addChild(child1); List children2 = bean1.getChildren(); @@ -413,32 +415,32 @@ public class SpringMetamodelIndexTest { void testSpringIndexStructurePolymorphicSerialization() { Gson gson = IndexCacheOnDiscDeltaBased.createGson(); - SubType2 subNode = new SubType2(); + QueryMethodIndexElement subNode = new QueryMethodIndexElement("find1", "SELECT * FROM All", null); - SubType1 node1 = new SubType1(); + ConfigPropertyIndexElement node1 = new ConfigPropertyIndexElement("prop1", "java.lang.String", null); node1.addChild(subNode); - SubType2 node2 = new SubType2(); + QueryMethodIndexElement node2 = new QueryMethodIndexElement("find", "SELECT * FROM S", null); - Root root = new Root(); + ProjectElement root = new ProjectElement("my-project"); root.addChild(node1); root.addChild(node2); String json = gson.toJson(root); - Root deserializedRoot = gson.fromJson(json, Root.class); + ProjectElement deserializedRoot = gson.fromJson(json, ProjectElement.class); List children = deserializedRoot.getChildren(); assertEquals(2, children.size()); - SubType1 deserializedNode1 = (SubType1) children.stream().filter(node -> node instanceof SubType1).findAny().get(); - SubType2 deserializedNode2 = (SubType2) children.stream().filter(node -> node instanceof SubType2).findAny().get(); + ConfigPropertyIndexElement deserializedNode1 = (ConfigPropertyIndexElement) children.stream().filter(node -> node instanceof ConfigPropertyIndexElement).findAny().get(); + QueryMethodIndexElement deserializedNode2 = (QueryMethodIndexElement) children.stream().filter(node -> node instanceof QueryMethodIndexElement).findAny().get(); assertNotNull(deserializedNode1); assertNotNull(deserializedNode2); List deserializedChild2 = deserializedNode1.getChildren(); assertEquals(1, deserializedChild2.size()); - assertTrue(deserializedChild2.get(0) instanceof SubType2); + assertTrue(deserializedChild2.get(0) instanceof QueryMethodIndexElement); } @Test @@ -470,11 +472,11 @@ public class SpringMetamodelIndexTest { Gson gson = IndexCacheOnDiscDeltaBased.createGson(); - SubType2 childOfChild = new SubType2(); - SubType1 child1 = new SubType1(); + QueryMethodIndexElement childOfChild = new QueryMethodIndexElement("find1", "SELECT * FROM All", null); + ConfigPropertyIndexElement child1 = new ConfigPropertyIndexElement("prop1", "java.lang.String", null); child1.addChild(childOfChild); - SubType2 child2 = new SubType2(); + QueryMethodIndexElement child2 = new QueryMethodIndexElement("find2", "SELECT s2 FROM S", null); Bean bean1 = new Bean("beanName1", "beanType", locationForDoc1, emptyInjectionPoints, emptySupertypes, emptyAnnotations, true, "symbolLabel"); bean1.addChild(child1); bean1.addChild(child2); @@ -485,14 +487,14 @@ public class SpringMetamodelIndexTest { List children = deserializedBean.getChildren(); assertEquals(2, children.size()); - SpringIndexElement deserializedChild1 = children.stream().filter(element -> element instanceof SubType1).findAny().get(); + SpringIndexElement deserializedChild1 = children.stream().filter(element -> element instanceof ConfigPropertyIndexElement).findAny().get(); assertNotNull(deserializedChild1); List childrenOfChild = deserializedChild1.getChildren(); assertEquals(1, childrenOfChild.size()); - assertTrue(childrenOfChild.get(0) instanceof SubType2); + assertTrue(childrenOfChild.get(0) instanceof QueryMethodIndexElement); - SpringIndexElement deserializedChild2 = children.stream().filter(element -> element instanceof SubType2).findAny().get(); + SpringIndexElement deserializedChild2 = children.stream().filter(element -> element instanceof QueryMethodIndexElement).findAny().get(); assertNotNull(deserializedChild2); assertEquals(0, deserializedChild2.getChildren().size()); } @@ -502,27 +504,18 @@ public class SpringMetamodelIndexTest { Gson gson = IndexCacheOnDiscDeltaBased.createGson(); - SubType1 child1 = new SubType1(); + ConfigPropertyIndexElement child1 = new ConfigPropertyIndexElement("prop1", "java.lang.String", null);; Bean bean1 = new Bean("beanName1", "beanType", locationForDoc1, emptyInjectionPoints, emptySupertypes, emptyAnnotations, true, "symbolLabel"); bean1.addChild(child1); String serialized = gson.toJson(bean1); Bean deserializedBean = gson.fromJson(serialized, Bean.class); - SubType2 newChild = new SubType2(); + QueryMethodIndexElement newChild = new QueryMethodIndexElement("find1", "SELECT * FROM All", null); deserializedBean.addChild(newChild); List childrenAfterNewChildAdded = deserializedBean.getChildren(); assertEquals(2, childrenAfterNewChildAdded.size()); } - static class SubType1 extends AbstractSpringIndexElement { - } - - static class SubType2 extends AbstractSpringIndexElement { - } - - static class Root extends AbstractSpringIndexElement { - } - } diff --git a/vscode-extensions/vscode-spring-boot/lib/Main.ts b/vscode-extensions/vscode-spring-boot/lib/Main.ts index aac637f4b..85ed3b9ad 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"; @@ -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)); - return new ApiManager(client).api; + const api = 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 93e6f3bfc..13bea525d 100644 --- a/vscode-extensions/vscode-spring-boot/lib/api.d.ts +++ b/vscode-extensions/vscode-spring-boot/lib/api.d.ts @@ -109,7 +109,6 @@ interface InjectionPoint { interface SpringIndex { readonly beans: (params: BeansParams) => Promise; - readonly getBeans: (uri: Uri) => Promise; readonly onSpringIndexUpdated: Event; } diff --git a/vscode-extensions/vscode-spring-boot/lib/apiManager.ts b/vscode-extensions/vscode-spring-boot/lib/apiManager.ts index 6422bab53..96009dd66 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, @@ -55,9 +55,6 @@ export class ApiManager { return await commands.executeCommand(COMMAND_LIVEDATA_REFRESH_METRICS, query); } - const COMMAND_BEANS = "sts/spring-boot/beans"; - const getBeans: (Uri) => Promise = async (projectUri: Uri) => await commands.executeCommand(COMMAND_BEANS, projectUri.toString()); - client.onNotification(LiveProcessConnectedNotification.type, (process: LiveProcess) => this.onDidLiveProcessConnectEmitter.fire(process)); client.onNotification(LiveProcessDisconnectedNotification.type, (process: LiveProcess) => this.onDidLiveProcessDisconnectEmitter.fire(process)); client.onNotification(LiveProcessUpdatedNotification.type, (process: LiveProcess) => this.onDidLiveProcessUpdateEmitter.fire(process)); @@ -73,8 +70,7 @@ export class ApiManager { const getSpringIndex = () => ({ onSpringIndexUpdated, - beans, - getBeans + beans }) this.api = { 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 000000000..0d6e481c8 --- /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 000000000..e14943850 --- /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 000000000..f9c41be4e --- /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 "ProjectElement": + return new ProjectNode(json.projectName as string, this.parseArray(json.children)); + case "DocumentElement": + return new DocumentNode(Uri.parse(json.docURI as string), this.parseArray(json.children)); + case "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 "AotProcessorElement": + return new AotProcessorNode( + this.parseArray(json.children), + json.name, + Uri.parse(json.docUri) + ); + case "BeanMethodContainerElement": + return new BeanMethodContainerNode( + this.parseArray(json.children), + json.type, + json.location + ); + case "BeanRegistrarElement": + return new BeanRegistrarNode( + this.parseArray(json.children), + json.name, + json.type, + json.location + + ); + case "ConfigPropertyIndexElement": + return new ConfigPropertyNode( + this.parseArray(json.children), + json.name, + json.type, + json.range + ); + case "EventListenerIndexElement": + return new EventListenerNode( + this.parseArray(json.children), + json.eventType, + json.location, + json.containerBeanType, + json.annotations + ); + case "EventPublisherIndexElement": + return new EventPublisherNode( + this.parseArray(json.children), + json.eventType, + json.location, + json.eventTypesFromHierarchy, + ); + case "QueryMethodIndexElement": + return new QueryMethodNode( + this.parseArray(json.children), + json.methodName, + json.queryString, + json.range + ); + case "RequestMappingIndexElement": + return new RequestMappingNode( + this.parseArray(json.children), + json.path, + json.httpMethods, + json.contentTypes, + json.acceptTypes, + json.symbolLabel, + json.range + ); + case "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