Skip to content

Commit 5789b3b

Browse files
committed
feat: improve application.yaml support
Signed-off-by: azerr <azerr@redhat.com>
1 parent c706d6b commit 5789b3b

12 files changed

+541
-201
lines changed

src/main/java/com/redhat/devtools/intellij/quarkus/QuarkusModuleUtil.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import com.intellij.openapi.roots.ModuleRootManager;
2020
import com.intellij.openapi.roots.OrderEnumerator;
2121
import com.intellij.openapi.roots.RootPolicy;
22-
import com.intellij.openapi.vfs.LocalFileSystem;
2322
import com.intellij.openapi.vfs.VfsUtil;
2423
import com.intellij.openapi.vfs.VirtualFile;
2524
import com.redhat.devtools.intellij.lsp4mp4ij.psi.core.project.PsiMicroProfileProject;
@@ -61,8 +60,8 @@ public class QuarkusModuleUtil {
6160
* @param module the module to check
6261
* @return true if module is a Quarkus project and false otherwise.
6362
*/
64-
public static boolean isQuarkusModule(Module module) {
65-
return hasLibrary(module, QuarkusConstants.QUARKUS_CORE_PREFIX);
63+
public static boolean isQuarkusModule(@Nullable Module module) {
64+
return module != null && hasLibrary(module, QuarkusConstants.QUARKUS_CORE_PREFIX);
6665
}
6766

6867
/**
@@ -139,8 +138,12 @@ public static boolean isQuarkusPropertiesFile(VirtualFile file, Project project)
139138
return false;
140139
}
141140

142-
public static boolean isQuarkusYAMLFile(VirtualFile file, Project project) {
143-
if (APPLICATION_YAML.matcher(file.getName()).matches()) {
141+
public static boolean isQuarkusYamlFile(@NotNull VirtualFile file) {
142+
return APPLICATION_YAML.matcher(file.getName()).matches();
143+
}
144+
145+
public static boolean isQuarkusYamlFile(VirtualFile file, Project project) {
146+
if (isQuarkusYamlFile(file)) {
144147
return isQuarkusModule(file, project);
145148
}
146149
return false;
@@ -214,4 +217,5 @@ private static int getPort(@NotNull PsiMicroProfileProject mpProject) {
214217
int port = mpProject.getPropertyAsInteger("quarkus.http.port", 8080);
215218
return mpProject.getPropertyAsInteger("%dev.quarkus.http.port", port);
216219
}
220+
217221
}
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2024 Red Hat Inc. and others.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v. 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
7+
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
8+
*
9+
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
10+
*
11+
* Contributors:
12+
* Red Hat Inc. - initial API and implementation
13+
*******************************************************************************/
14+
package com.redhat.devtools.intellij.quarkus.json;
15+
16+
import com.intellij.openapi.application.ApplicationManager;
17+
import com.intellij.openapi.module.Module;
18+
import com.intellij.openapi.progress.EmptyProgressIndicator;
19+
import com.intellij.openapi.project.DumbService;
20+
import com.intellij.openapi.project.Project;
21+
import com.intellij.openapi.util.Key;
22+
import com.intellij.openapi.vfs.VirtualFile;
23+
import com.jetbrains.jsonSchema.extension.JsonSchemaFileProvider;
24+
import com.jetbrains.jsonSchema.extension.SchemaType;
25+
import com.jetbrains.jsonSchema.impl.JsonSchemaVersion;
26+
import com.redhat.devtools.intellij.lsp4mp4ij.psi.core.PropertiesManager;
27+
import com.redhat.devtools.intellij.lsp4mp4ij.psi.internal.core.ls.PsiUtilsLSImpl;
28+
import com.redhat.devtools.intellij.quarkus.ProgressIndicatorWrapper;
29+
import com.redhat.devtools.intellij.quarkus.QuarkusModuleUtil;
30+
import com.redhat.devtools.lsp4ij.LSPIJUtils;
31+
import org.eclipse.lsp4mp.commons.ClasspathKind;
32+
import org.eclipse.lsp4mp.commons.DocumentFormat;
33+
import org.eclipse.lsp4mp.commons.MicroProfileProjectInfo;
34+
import org.eclipse.lsp4mp.commons.MicroProfilePropertiesScope;
35+
import org.eclipse.lsp4mp.utils.JSONSchemaUtils;
36+
import org.jetbrains.annotations.NotNull;
37+
import org.jetbrains.annotations.Nullable;
38+
import org.slf4j.Logger;
39+
import org.slf4j.LoggerFactory;
40+
41+
import java.util.Objects;
42+
43+
/**
44+
* Json Schema provider used to provide completion, validation, hover in application.yaml.
45+
*/
46+
public class ApplicationYamlJsonSchemaFileProvider implements JsonSchemaFileProvider {
47+
48+
private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationYamlJsonSchemaFileProvider.class);
49+
50+
private static final Key<ApplicationYamlJsonSchemaFileProvider> JSON_SCHEMA_PROVIDER = Key.create("quarkus.application.yaml.schema.for.module");
51+
52+
private final String jsonFilename;
53+
private final @NotNull Project project;
54+
private final @NotNull JsonSchemaLightVirtualFile jsonSchemaFile;
55+
private @Nullable Module module;
56+
private boolean updating;
57+
private long updatedTime;
58+
private long toUpdateTime;
59+
60+
/**
61+
* LSP Server / Configuration {@link com.jetbrains.jsonSchema.extension.JsonSchemaFileProvider} constructor.
62+
*
63+
* @param index the index where the provider instance is stored in the pool of providers managed by {@link ApplicationYamlJsonSchemaManager}.
64+
* @param project the project.
65+
*/
66+
public ApplicationYamlJsonSchemaFileProvider(int index, @NotNull Project project) {
67+
this.jsonFilename = generateJsonFileName(index);
68+
this.project = project;
69+
this.jsonSchemaFile = new JsonSchemaLightVirtualFile(generateJsonSchemaFileName(index), "");
70+
this.module = null;
71+
}
72+
73+
@Override
74+
public boolean isAvailable(@NotNull VirtualFile file) {
75+
// Is file name matches application.yaml ?
76+
if (!QuarkusModuleUtil.isQuarkusYamlFile(file)) {
77+
return false;
78+
}
79+
// Is module is a Quarkus project?
80+
Module module = LSPIJUtils.getModule(file, project);
81+
if (!QuarkusModuleUtil.isQuarkusModule(module)) {
82+
return false;
83+
}
84+
// Is module is already associated to a Json Schema provider?
85+
var provider = getProviderFor(module);
86+
if (provider != null && provider != this) {
87+
return false;
88+
}
89+
if (this.module == null) {
90+
// Associate a Json Schema provider to the module
91+
module.putUserData(JSON_SCHEMA_PROVIDER, this);
92+
this.module = module;
93+
reset();
94+
return true;
95+
}
96+
return module.equals(this.module);
97+
}
98+
99+
/**
100+
* Returns the associated Json provider to the given module and null otherwise.
101+
* @param module the module.
102+
* @return the associated Json provider to the given module and null otherwise.
103+
*/
104+
@Nullable
105+
public static ApplicationYamlJsonSchemaFileProvider getProviderFor(@NotNull Module module) {
106+
return module.getUserData(JSON_SCHEMA_PROVIDER);
107+
}
108+
109+
@Nullable
110+
@Override
111+
public final VirtualFile getSchemaFile() {
112+
if (module != null) {
113+
updateJsonSchemaIfNeededFor(module);
114+
}
115+
return jsonSchemaFile;
116+
}
117+
118+
@NotNull
119+
@Override
120+
public final String getName() {
121+
return jsonFilename;
122+
}
123+
124+
@NotNull
125+
@Override
126+
public final SchemaType getSchemaType() {
127+
return SchemaType.schema;
128+
}
129+
130+
@Override
131+
public final JsonSchemaVersion getSchemaVersion() {
132+
return JsonSchemaVersion.SCHEMA_7;
133+
}
134+
135+
@NotNull
136+
@Override
137+
public final String getPresentableName() {
138+
return getName();
139+
}
140+
141+
@Override
142+
public boolean isUserVisible() {
143+
return false;
144+
}
145+
146+
protected static void reloadPsi(@Nullable VirtualFile file) {
147+
if (file != null) {
148+
file.refresh(true, false, () -> file.refresh(false, false));
149+
}
150+
}
151+
152+
private static String generateJsonFileName(int index) {
153+
return "quarkus.application.yaml." + index + ".json";
154+
}
155+
156+
private static String generateJsonSchemaFileName(int index) {
157+
return "quarkus.application.yaml." + index + ".schema.json";
158+
}
159+
160+
/**
161+
* Free the Json Schema provider (when Java sources , Libraries changed) to evict the cached Json Schema.
162+
*/
163+
public void reset() {
164+
toUpdateTime = System.currentTimeMillis();
165+
}
166+
167+
/**
168+
* Update file content.
169+
*
170+
* @param content the new content.
171+
* @param file the file to update.
172+
*/
173+
private static void updateFileContent(@NotNull String content,
174+
@NotNull JsonSchemaLightVirtualFile file) {
175+
if (Objects.equals(content, file.getContent())) {
176+
// No changes, don't update the file.
177+
return;
178+
}
179+
// Update the virtual file content and the modification stamp (used by Json Schema cache)
180+
file.setContent(content);
181+
// Synchronize the Psi file from the new content of the virtual file and the modification stamp (used by Json Schema cache)
182+
reloadPsi(file);
183+
}
184+
185+
/**
186+
* Update Json Schema for teh given module if needed.
187+
* @param module the module.
188+
*/
189+
private void updateJsonSchemaIfNeededFor(@NotNull Module module) {
190+
if (updating || toUpdateTime == updatedTime) {
191+
// - the Json schema is updating
192+
// - or the Json Schema doesn't need to be updated
193+
return;
194+
}
195+
updating = true;
196+
updatedTime = toUpdateTime;
197+
final long currentUpdatedTime = updatedTime;
198+
var project = module.getProject();
199+
// Generate and update the Json Schema when indexed files are finished.
200+
DumbService.getInstance(project)
201+
.runWhenSmart(() -> {
202+
// Check if previous start of Json Schema update is relevant
203+
if (isCanceled(currentUpdatedTime)) {
204+
return;
205+
}
206+
ApplicationManager.getApplication()
207+
.runWriteAction(() -> {
208+
// Check if previous start of Json Schema update is relevant
209+
if (isCanceled(currentUpdatedTime)) {
210+
return;
211+
}
212+
try {
213+
// Collect all MicroProfile/Quarksu properties from the given module.
214+
MicroProfileProjectInfo info = PropertiesManager.getInstance().getMicroProfileProjectInfo(module,
215+
MicroProfilePropertiesScope.SOURCES_AND_DEPENDENCIES, ClasspathKind.TEST, PsiUtilsLSImpl.getInstance(project),
216+
DocumentFormat.Markdown, new ProgressIndicatorWrapper(new EmptyProgressIndicator()) {
217+
@Override
218+
public boolean isCanceled() {
219+
return super.isCanceled() || ApplicationYamlJsonSchemaFileProvider.this.isCanceled(currentUpdatedTime);
220+
}
221+
});
222+
// Generate Json Schema
223+
String schemaContent = JSONSchemaUtils.toJSONSchema(info, false);
224+
if (isCanceled(currentUpdatedTime)) {
225+
return;
226+
}
227+
// Update file with the generated Json Schema
228+
updateFileContent(schemaContent, jsonSchemaFile);
229+
} catch (Exception e) {
230+
LOGGER.error("Error while generating Quarkus Json Schema for the module '{}.", module.getName(), e);
231+
} finally {
232+
updating = false;
233+
}
234+
});
235+
});
236+
}
237+
238+
private boolean isCanceled(long currentUpdatedTime) {
239+
return updatedTime != currentUpdatedTime;
240+
}
241+
242+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2024 Red Hat Inc. and others.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v. 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
7+
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
8+
*
9+
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
10+
*
11+
* Contributors:
12+
* Red Hat Inc. - initial API and implementation
13+
*******************************************************************************/
14+
package com.redhat.devtools.intellij.quarkus.json;
15+
16+
import com.intellij.openapi.Disposable;
17+
import com.intellij.openapi.module.Module;
18+
import com.intellij.openapi.project.Project;
19+
import com.intellij.openapi.util.Pair;
20+
import com.intellij.openapi.vfs.VirtualFile;
21+
import com.intellij.util.messages.MessageBusConnection;
22+
import com.redhat.devtools.intellij.lsp4mp4ij.classpath.ClasspathResourceChangedManager;
23+
import com.redhat.devtools.intellij.lsp4mp4ij.psi.core.project.PsiMicroProfileProjectManager;
24+
import com.redhat.devtools.intellij.quarkus.QuarkusPluginDisposable;
25+
import com.redhat.devtools.lsp4ij.settings.jsonSchema.LSPJsonSchemaProviderFactory;
26+
import org.jetbrains.annotations.NotNull;
27+
import org.jetbrains.annotations.Nullable;
28+
import org.slf4j.Logger;
29+
import org.slf4j.LoggerFactory;
30+
31+
import java.util.ArrayList;
32+
import java.util.List;
33+
import java.util.Set;
34+
import java.util.stream.Collectors;
35+
36+
/**
37+
* Pooling of JsonSchemaProvider used by Server / Configuration editors.
38+
* We need this pooling because there are no way to register {@link com.jetbrains.jsonSchema.extension.JsonSchemaFileProvider}
39+
* dynamically with {@link LSPJsonSchemaProviderFactory}.
40+
*/
41+
public class ApplicationYamlJsonSchemaManager implements ClasspathResourceChangedManager.Listener, Disposable {
42+
43+
private static final int JSON_SCHEMA_FILE_PROVIDER_POOL_SIZE = 20;
44+
45+
public static ApplicationYamlJsonSchemaManager getInstance(@NotNull Project project) {
46+
return project.getService(ApplicationYamlJsonSchemaManager.class);
47+
}
48+
49+
private final List<ApplicationYamlJsonSchemaFileProvider> providers;
50+
private final MessageBusConnection connection;
51+
52+
public ApplicationYamlJsonSchemaManager(@NotNull Project project) {
53+
providers = new ArrayList<>();
54+
// Create 100 dummy ApplicationYamlJsonSchemaFileProvider
55+
for (int i = 0; i < JSON_SCHEMA_FILE_PROVIDER_POOL_SIZE; i++) {
56+
providers.add(new ApplicationYamlJsonSchemaFileProvider(i, project));
57+
}
58+
connection = project.getMessageBus().connect(QuarkusPluginDisposable.getInstance(project));
59+
connection.subscribe(ClasspathResourceChangedManager.TOPIC, this);
60+
}
61+
62+
public List<ApplicationYamlJsonSchemaFileProvider> getProviders() {
63+
return providers;
64+
}
65+
66+
/**
67+
* Free the {@link ApplicationYamlJsonSchemaFileProvider} stored at the given index from the pool
68+
* (when a Server / Configuration editor is disposed).
69+
*
70+
* @param index the index of {@link ApplicationYamlJsonSchemaFileProvider}.
71+
*/
72+
public void reset(@NotNull Integer index) {
73+
ApplicationYamlJsonSchemaFileProvider provider = getProviders().get(index);
74+
provider.reset();
75+
}
76+
77+
@Override
78+
public void librariesChanged() {
79+
for(var provider : getProviders()) {
80+
provider.reset();
81+
}
82+
}
83+
84+
@Override
85+
public void sourceFilesChanged(Set<Pair<VirtualFile, Module>> sources) {
86+
Set<Module> modules = sources
87+
.stream()
88+
.filter(source -> isJavaFile(source.getFirst()))
89+
.map(source -> source.getSecond())
90+
.collect(Collectors.toSet());
91+
for (var module : modules ) {
92+
var provider = ApplicationYamlJsonSchemaFileProvider.getProviderFor(module);
93+
if (provider != null) {
94+
provider.reset();
95+
}
96+
}
97+
}
98+
99+
private boolean isJavaFile(@NotNull VirtualFile file) {
100+
return PsiMicroProfileProjectManager.isJavaFile(file);
101+
}
102+
103+
@Override
104+
public void dispose() {
105+
connection.disconnect();
106+
}
107+
108+
}

0 commit comments

Comments
 (0)