From 260e9c9d49626eb5fbeea30706d2239d272a2829 Mon Sep 17 00:00:00 2001 From: aboyko Date: Wed, 9 Apr 2025 10:56:17 -0400 Subject: [PATCH 1/5] WIP Signed-off-by: aboyko --- .../ide/vscode/boot/app/CommandsConfig.java | 9 ++++++- .../java/commands/SpringIndexCommands.java | 27 +++++++++++++++++++ .../vscode-spring-boot/lib/Main.ts | 4 +-- .../vscode-spring-boot/lib/api.d.ts | 3 ++- .../vscode-spring-boot/lib/apiManager.ts | 6 ++++- 5 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/SpringIndexCommands.java 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/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..409e223dee --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/SpringIndexCommands.java @@ -0,0 +1,27 @@ +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"; + + 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())); + }); + } + +} diff --git a/vscode-extensions/vscode-spring-boot/lib/Main.ts b/vscode-extensions/vscode-spring-boot/lib/Main.ts index c5ae4278d9..aac637f4bf 100644 --- a/vscode-extensions/vscode-spring-boot/lib/Main.ts +++ b/vscode-extensions/vscode-spring-boot/lib/Main.ts @@ -52,8 +52,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()) { diff --git a/vscode-extensions/vscode-spring-boot/lib/api.d.ts b/vscode-extensions/vscode-spring-boot/lib/api.d.ts index 863a6d99e5..93e6f3bfca 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"; @@ -109,6 +109,7 @@ 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 8462c75997..6422bab533 100644 --- a/vscode-extensions/vscode-spring-boot/lib/apiManager.ts +++ b/vscode-extensions/vscode-spring-boot/lib/apiManager.ts @@ -55,6 +55,9 @@ 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)); @@ -70,7 +73,8 @@ export class ApiManager { const getSpringIndex = () => ({ onSpringIndexUpdated, - beans + beans, + getBeans }) this.api = { From 8ccaa5f5ef30f614ba76cf11949e3e021dee3773 Mon Sep 17 00:00:00 2001 From: aboyko Date: Tue, 15 Apr 2025 16:58:45 -0400 Subject: [PATCH 2/5] `SpringIndexElement` serialization. Initial Structure view in VSCode Signed-off-by: aboyko --- .../languageserver/LanguageServerRunner.java | 15 +- .../commons/RuntimeTypeAdapterFactory.java | 331 ++++++++++++++++++ .../starter/LanguageServerRunnerAutoConf.java | 11 +- .../boot/app/BootLanguageServerBootApp.java | 33 ++ .../cache/IndexCacheOnDiscDeltaBased.java | 141 ++------ .../java/commands/SpringIndexCommands.java | 16 +- .../boot/java/cron/JdtCronVisitorUtils.java | 10 +- .../index/test/SpringMetamodelIndexTest.java | 51 ++- .../vscode-spring-boot/lib/Main.ts | 48 ++- .../vscode-spring-boot/lib/api.d.ts | 1 - .../vscode-spring-boot/lib/apiManager.ts | 10 +- .../lib/explorer/explorer-tree-provider.ts | 40 +++ .../vscode-spring-boot/lib/explorer/nodes.ts | 222 ++++++++++++ .../lib/explorer/structure-tree-manager.ts | 124 +++++++ 14 files changed, 873 insertions(+), 180 deletions(-) create mode 100644 headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/RuntimeTypeAdapterFactory.java create mode 100644 vscode-extensions/vscode-spring-boot/lib/explorer/explorer-tree-provider.ts create mode 100644 vscode-extensions/vscode-spring-boot/lib/explorer/nodes.ts create mode 100644 vscode-extensions/vscode-spring-boot/lib/explorer/structure-tree-manager.ts 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..8d67f658e2 --- /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 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..eb083d24f6 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; @@ -45,6 +46,7 @@ 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.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.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.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.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 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 @@ ModulithService modulithService(SimpleLanguageServer server, JavaProjectFinder p 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 503b7466bf..8da2a2aaa8 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 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 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() + .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 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 index 409e223dee..1a2dd7d851 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 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/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 26c37b6ecb..ae3740a8b3 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.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 @@ void testOverallSerializeDeserializeBeans() { 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 @@ void testEmptyInjectionPointsOptimizationWithSerializeDeserializeBeans() { 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 @@ void testFindMatchingBeansWithMultipleProjects() { 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 @@ void testBasicSpringIndexStructure() { 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 @@ void testSerializeDeserializeIndexElementsWithChildElements() { 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 @@ void testSerializeDeserializeIndexElementsWithChildElements() { 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 @@ void testAddChildAfterDeserialize() { 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 aac637f4bf..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"; @@ -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 93e6f3bfca..13bea525d8 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 6422bab533..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, @@ -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 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..f9c41be4e4 --- /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 From fe0c738d1bc043b5e54b6cf89cfa990ad9d99187 Mon Sep 17 00:00:00 2001 From: aboyko Date: Tue, 15 Apr 2025 17:07:59 -0400 Subject: [PATCH 3/5] Corrections Signed-off-by: aboyko --- .../ide/vscode/commons/RuntimeTypeAdapterFactory.java | 2 +- vscode-extensions/vscode-spring-boot/package.json | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) 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 index 8d67f658e2..dc05582292 100644 --- 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 @@ -5,7 +5,7 @@ * 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 + * 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, 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", From c56fc655320b4747b4764826b145336634b2996f Mon Sep 17 00:00:00 2001 From: aboyko Date: Wed, 16 Apr 2025 14:08:47 -0400 Subject: [PATCH 4/5] Serialization polishing Signed-off-by: aboyko --- .../commons/RuntimeTypeAdapterFactory.java | 415 ++++++++++-------- .../vscode/commons/protocol/spring/Bean.java | 17 +- .../protocol/spring/InjectionPoint.java | 6 +- .../boot/app/BootLanguageServerBootApp.java | 25 +- .../cache/IndexCacheOnDiscDeltaBased.java | 28 +- .../index/test/SpringMetamodelIndexTest.java | 51 ++- .../lib/explorer/structure-tree-manager.ts | 24 +- 7 files changed, 274 insertions(+), 292 deletions(-) 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 index dc05582292..51159d2314 100644 --- 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 @@ -16,6 +16,11 @@ 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; @@ -27,39 +32,41 @@ 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: + * 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;
+ * 	int x;
+ * 	int y;
  * }
+ * 
  * class Circle extends Shape {
- *   int radius;
+ * 	int radius;
  * }
+ * 
  * class Rectangle extends Shape {
- *   int width;
- *   int height;
+ * 	int width;
+ * 	int height;
  * }
+ * 
  * class Diamond extends Shape {
- *   int width;
- *   int height;
+ * 	int width;
+ * 	int height;
  * }
+ * 
  * class Drawing {
- *   Shape bottomShape;
- *   Shape topShape;
+ * 	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? + *

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

{@code
  * {
@@ -77,8 +84,9 @@
  * }
  * }
* - * This class addresses this problem by adding type information to the serialized JSON and honoring - * that type information when the JSON is deserialized: + * This class addresses this problem by adding type information to the + * serialized JSON and honoring that type information when the JSON is + * deserialized: * *
{@code
  * {
@@ -98,23 +106,22 @@
  * }
  * }
* - * Both the type field name ({@code "type"}) and the type labels ({@code "Rectangle"}) are - * configurable. + * 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. + * 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");
+ * 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. + * 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");
@@ -122,27 +129,24 @@
  * shapeAdapterFactory.registerSubtype(Diamond.class, "Diamond");
  * }
* - * Finally, register the type adapter factory in your application's GSON builder: + * Finally, register the type adapter factory in your application's GSON + * builder: * *
{@code
- * Gson gson = new GsonBuilder()
- *     .registerTypeAdapterFactory(shapeAdapterFactory)
- *     .create();
+ * 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);
+ * 		.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. + * In order to serialize and deserialize a polymorphic object, you must specify + * the base type explicitly. * *
{@code
  * Diamond diamond = new Diamond();
@@ -156,176 +160,199 @@
  * }
*/ 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 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; - } + 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. + * + * @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 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); - } + /** + * 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; - } + /** + * 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 {@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()); + } - /** - * 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; + } - @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() { - 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); - } + 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; + } - 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); - } + @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(); - @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); - } + 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); + } - @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(); + @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; - } + if (maintainType) { + jsonElementAdapter.write(out, jsonObject); + return; + } - JsonObject clone = new JsonObject(); + 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)); + 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(); - } + 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..fd18880cd2 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 @@ -46,24 +46,21 @@ public Bean( this.symbolLabel = symbolLabel; 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; + this.supertypes = null; } else { this.supertypes = supertypes; } if (annotations != null && annotations.length == 0) { - this.annotations = DefaultValues.EMPTY_ANNOTATIONS; + this.annotations = null; } else { this.annotations = annotations; @@ -83,15 +80,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)) || (getSupertypes().contains(type))); } public AnnotationMetadata[] getAnnotations() { - return annotations; + return annotations == null ? DefaultValues.EMPTY_ANNOTATIONS : annotations; } public boolean isConfiguration() { @@ -99,7 +96,7 @@ public boolean isConfiguration() { } public Set getSupertypes() { - return supertypes; + return supertypes == null ? DefaultValues.EMPTY_SUPERTYPES : supertypes; } 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/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 eb083d24f6..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 @@ -46,7 +46,6 @@ 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; @@ -54,11 +53,8 @@ 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; @@ -78,8 +74,6 @@ 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; @@ -106,11 +100,6 @@ 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; @@ -447,18 +436,6 @@ ModulithService modulithService(SimpleLanguageServer server, JavaProjectFinder p 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)); + .recognizeSubtypes()); } } 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 8da2a2aaa8..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 @@ -40,19 +40,7 @@ import org.apache.commons.lang3.tuple.Pair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -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.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; @@ -579,21 +567,7 @@ public static Gson createGson() { .registerTypeAdapter(DeltaStorage.class, new DeltaStorageAdapter()) .registerTypeAdapter(IndexCacheStore.class, new IndexCacheStoreAdapter()) .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)) - - + .recognizeSubtypes()) .create(); } 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 ae3740a8b3..26c37b6ecb 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,14 +29,12 @@ 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.boot.java.beans.ConfigPropertyIndexElement; -import org.springframework.ide.vscode.boot.java.data.QueryMethodIndexElement; +import org.springframework.ide.vscode.commons.protocol.spring.AbstractSpringIndexElement; 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; @@ -271,7 +269,7 @@ void testOverallSerializeDeserializeBeans() { assertEquals("point2", points[1].getName()); assertEquals("point2-type", points[1].getType()); assertEquals(locationForDoc1, points[1].getLocation()); - assertEquals(0, points[1].getAnnotations().length); + assertSame(DefaultValues.EMPTY_ANNOTATIONS, points[1].getAnnotations()); assertTrue(deserializedBean.isTypeCompatibleWith("supertype1")); assertTrue(deserializedBean.isTypeCompatibleWith("supertype2")); @@ -311,7 +309,7 @@ void testEmptyInjectionPointsOptimizationWithSerializeDeserializeBeans() { assertEquals("beanType", deserializedBean.getType()); assertEquals(locationForDoc1, deserializedBean.getLocation()); - assertEquals(0, deserializedBean.getInjectionPoints().length); + assertSame(DefaultValues.EMPTY_INJECTION_POINTS, deserializedBean.getInjectionPoints()); } @Test @@ -403,7 +401,7 @@ void testFindMatchingBeansWithMultipleProjects() { void testBasicSpringIndexStructure() { Bean bean1 = new Bean("beanName1", "beanType1", locationForDoc1, emptyInjectionPoints, Set.of("supertype1", "supertype2"), emptyAnnotations, false, "symbolLabel"); - ConfigPropertyIndexElement child1 = new ConfigPropertyIndexElement("prop1", "java.lang.String", null); + SubType1 child1 = new SubType1(); bean1.addChild(child1); List children2 = bean1.getChildren(); @@ -415,32 +413,32 @@ void testBasicSpringIndexStructure() { void testSpringIndexStructurePolymorphicSerialization() { Gson gson = IndexCacheOnDiscDeltaBased.createGson(); - QueryMethodIndexElement subNode = new QueryMethodIndexElement("find1", "SELECT * FROM All", null); + SubType2 subNode = new SubType2(); - ConfigPropertyIndexElement node1 = new ConfigPropertyIndexElement("prop1", "java.lang.String", null); + SubType1 node1 = new SubType1(); node1.addChild(subNode); - QueryMethodIndexElement node2 = new QueryMethodIndexElement("find", "SELECT * FROM S", null); + SubType2 node2 = new SubType2(); - ProjectElement root = new ProjectElement("my-project"); + Root root = new Root(); root.addChild(node1); root.addChild(node2); String json = gson.toJson(root); - ProjectElement deserializedRoot = gson.fromJson(json, ProjectElement.class); + Root deserializedRoot = gson.fromJson(json, Root.class); List children = deserializedRoot.getChildren(); assertEquals(2, children.size()); - ConfigPropertyIndexElement deserializedNode1 = (ConfigPropertyIndexElement) children.stream().filter(node -> node instanceof ConfigPropertyIndexElement).findAny().get(); - QueryMethodIndexElement deserializedNode2 = (QueryMethodIndexElement) children.stream().filter(node -> node instanceof QueryMethodIndexElement).findAny().get(); + SubType1 deserializedNode1 = (SubType1) children.stream().filter(node -> node instanceof SubType1).findAny().get(); + SubType2 deserializedNode2 = (SubType2) children.stream().filter(node -> node instanceof SubType2).findAny().get(); assertNotNull(deserializedNode1); assertNotNull(deserializedNode2); List deserializedChild2 = deserializedNode1.getChildren(); assertEquals(1, deserializedChild2.size()); - assertTrue(deserializedChild2.get(0) instanceof QueryMethodIndexElement); + assertTrue(deserializedChild2.get(0) instanceof SubType2); } @Test @@ -472,11 +470,11 @@ void testSerializeDeserializeIndexElementsWithChildElements() { Gson gson = IndexCacheOnDiscDeltaBased.createGson(); - QueryMethodIndexElement childOfChild = new QueryMethodIndexElement("find1", "SELECT * FROM All", null); - ConfigPropertyIndexElement child1 = new ConfigPropertyIndexElement("prop1", "java.lang.String", null); + SubType2 childOfChild = new SubType2(); + SubType1 child1 = new SubType1(); child1.addChild(childOfChild); - QueryMethodIndexElement child2 = new QueryMethodIndexElement("find2", "SELECT s2 FROM S", null); + SubType2 child2 = new SubType2(); Bean bean1 = new Bean("beanName1", "beanType", locationForDoc1, emptyInjectionPoints, emptySupertypes, emptyAnnotations, true, "symbolLabel"); bean1.addChild(child1); bean1.addChild(child2); @@ -487,14 +485,14 @@ void testSerializeDeserializeIndexElementsWithChildElements() { List children = deserializedBean.getChildren(); assertEquals(2, children.size()); - SpringIndexElement deserializedChild1 = children.stream().filter(element -> element instanceof ConfigPropertyIndexElement).findAny().get(); + SpringIndexElement deserializedChild1 = children.stream().filter(element -> element instanceof SubType1).findAny().get(); assertNotNull(deserializedChild1); List childrenOfChild = deserializedChild1.getChildren(); assertEquals(1, childrenOfChild.size()); - assertTrue(childrenOfChild.get(0) instanceof QueryMethodIndexElement); + assertTrue(childrenOfChild.get(0) instanceof SubType2); - SpringIndexElement deserializedChild2 = children.stream().filter(element -> element instanceof QueryMethodIndexElement).findAny().get(); + SpringIndexElement deserializedChild2 = children.stream().filter(element -> element instanceof SubType2).findAny().get(); assertNotNull(deserializedChild2); assertEquals(0, deserializedChild2.getChildren().size()); } @@ -504,18 +502,27 @@ void testAddChildAfterDeserialize() { Gson gson = IndexCacheOnDiscDeltaBased.createGson(); - ConfigPropertyIndexElement child1 = new ConfigPropertyIndexElement("prop1", "java.lang.String", null);; + SubType1 child1 = new SubType1(); 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); - QueryMethodIndexElement newChild = new QueryMethodIndexElement("find1", "SELECT * FROM All", null); + SubType2 newChild = new SubType2(); 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/explorer/structure-tree-manager.ts b/vscode-extensions/vscode-spring-boot/lib/explorer/structure-tree-manager.ts index f9c41be4e4..2348c9d0b6 100644 --- a/vscode-extensions/vscode-spring-boot/lib/explorer/structure-tree-manager.ts +++ b/vscode-extensions/vscode-spring-boot/lib/explorer/structure-tree-manager.ts @@ -23,11 +23,11 @@ export class StructureManager { private parseNode(json: any): SpringNode | undefined { if (typeof (json._internal_node_type) === 'string') { switch (json._internal_node_type) { - case "ProjectElement": + case "org.springframework.ide.vscode.commons.protocol.spring.ProjectElement": return new ProjectNode(json.projectName as string, this.parseArray(json.children)); - case "DocumentElement": + case "org.springframework.ide.vscode.commons.protocol.spring.DocumentElement": return new DocumentNode(Uri.parse(json.docURI as string), this.parseArray(json.children)); - case "Bean": + case "org.springframework.ide.vscode.commons.protocol.spring.Bean": return new BeanNode( this.parseArray(json.children), json.name, @@ -39,19 +39,19 @@ export class StructureManager { json.isConfiguration, json.symbolLabel ); - case "AotProcessorElement": + case "org.springframework.ide.vscode.commons.protocol.spring.AotProcessorElement": return new AotProcessorNode( this.parseArray(json.children), json.name, Uri.parse(json.docUri) ); - case "BeanMethodContainerElement": + case "org.springframework.ide.vscode.commons.protocol.spring.BeanMethodContainerElement": return new BeanMethodContainerNode( this.parseArray(json.children), json.type, json.location ); - case "BeanRegistrarElement": + case "org.springframework.ide.vscode.commons.protocol.spring.BeanRegistrarElement": return new BeanRegistrarNode( this.parseArray(json.children), json.name, @@ -59,14 +59,14 @@ export class StructureManager { json.location ); - case "ConfigPropertyIndexElement": + case "org.springframework.ide.vscode.boot.java.beans.ConfigPropertyIndexElement": return new ConfigPropertyNode( this.parseArray(json.children), json.name, json.type, json.range ); - case "EventListenerIndexElement": + case "org.springframework.ide.vscode.boot.java.events.EventListenerIndexElement": return new EventListenerNode( this.parseArray(json.children), json.eventType, @@ -74,21 +74,21 @@ export class StructureManager { json.containerBeanType, json.annotations ); - case "EventPublisherIndexElement": + case "org.springframework.ide.vscode.boot.java.events.EventPublisherIndexElement": return new EventPublisherNode( this.parseArray(json.children), json.eventType, json.location, json.eventTypesFromHierarchy, ); - case "QueryMethodIndexElement": + case "org.springframework.ide.vscode.boot.java.data.QueryMethodIndexElement": return new QueryMethodNode( this.parseArray(json.children), json.methodName, json.queryString, json.range ); - case "RequestMappingIndexElement": + case "org.springframework.ide.vscode.boot.java.requestmapping.RequestMappingIndexElement": return new RequestMappingNode( this.parseArray(json.children), json.path, @@ -98,7 +98,7 @@ export class StructureManager { json.symbolLabel, json.range ); - case "WebfluxRouteElementRangesIndexElement": + case "org.springframework.ide.vscode.boot.java.requestmapping.WebfluxRouteElementRangesIndexElement": return new WebfluxRoutesNode( this.parseArray(json.children), json.path, From 6cc3086b0e2a73d2e66d38b41536c0247802cec1 Mon Sep 17 00:00:00 2001 From: aboyko Date: Wed, 16 Apr 2025 23:47:58 -0400 Subject: [PATCH 5/5] Don't include java.lang.Object in supertypes serialization Signed-off-by: aboyko --- .../vscode/commons/protocol/spring/Bean.java | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) 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 fd18880cd2..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,6 +47,7 @@ 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 = null; @@ -52,11 +56,12 @@ public Bean( this.injectionPoints = injectionPoints; } - if (supertypes != null && supertypes.size() == 0) { + 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) { @@ -84,7 +89,7 @@ public InjectionPoint[] getInjectionPoints() { } public boolean isTypeCompatibleWith(String type) { - return type != null && ((this.type != null && this.type.equals(type)) || (getSupertypes().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() { @@ -96,7 +101,11 @@ public boolean isConfiguration() { } public Set getSupertypes() { - return supertypes == null ? DefaultValues.EMPTY_SUPERTYPES : 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() {