Skip to content

Commit 2ca993b

Browse files
martinlippertBoykoAlex
authored andcommitted
GH-1427: initial support for jmolecules stereotypes
1 parent 46584e7 commit 2ca993b

36 files changed

+1666
-3
lines changed

headless-services/spring-boot-language-server/pom.xml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,18 @@
5050
<enabled>false</enabled>
5151
</releases>
5252
</repository>
53+
54+
<repository>
55+
<id>jmolecules-snapshots</id>
56+
<url>https://s01.oss.sonatype.org/content/repositories/snapshots</url>
57+
<snapshots>
58+
<enabled>true</enabled>
59+
</snapshots>
60+
<releases>
61+
<enabled>false</enabled>
62+
</releases>
63+
</repository>
64+
5365
</repositories>
5466

5567
<dependencies>
@@ -122,6 +134,7 @@
122134
<artifactId>commons-rewrite</artifactId>
123135
<version>${dependencies.version}</version>
124136
</dependency>
137+
125138
<dependency>
126139
<groupId>org.eclipse.jdt</groupId>
127140
<artifactId>org.eclipse.jdt.core</artifactId>
@@ -146,6 +159,12 @@
146159
<groupId>org.apache.commons</groupId>
147160
<artifactId>commons-text</artifactId>
148161
</dependency>
162+
163+
<dependency>
164+
<groupId>org.jmolecules.integrations</groupId>
165+
<artifactId>jmolecules-stereotype</artifactId>
166+
<version>0.27.0-STEREOTYPE-SNAPSHOT</version>
167+
</dependency>
149168

150169

151170
<dependency>

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler;
7979
import org.springframework.ide.vscode.boot.java.reconcilers.JdtReconciler;
8080
import org.springframework.ide.vscode.boot.java.spel.SpelDefinitionProvider;
81+
import org.springframework.ide.vscode.boot.java.stereotypes.StereotypeCatalogRegistry;
8182
import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache;
8283
import org.springframework.ide.vscode.boot.java.value.ValueDefinitionProvider;
8384
import org.springframework.ide.vscode.boot.jdt.ls.JavaProjectsService;
@@ -440,6 +441,11 @@ DataRepositoryAotMetadataService dataAotMetadataService() {
440441
return new DataRepositoryAotMetadataService();
441442
}
442443

444+
@Bean
445+
StereotypeCatalogRegistry stereotypeCatalogRegistry() {
446+
return new StereotypeCatalogRegistry();
447+
}
448+
443449
@Bean ResponseModifier responseModifier() {
444450
return new ResponseModifier();
445451
}

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/SpringSymbolIndexerConfig.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@
2525
import org.springframework.ide.vscode.boot.java.handlers.SymbolProvider;
2626
import org.springframework.ide.vscode.boot.java.requestmapping.HttpExchangeSymbolProvider;
2727
import org.springframework.ide.vscode.boot.java.requestmapping.RequestMappingSymbolProvider;
28+
import org.springframework.ide.vscode.boot.java.stereotypes.StereotypeCatalogRegistry;
29+
import org.springframework.ide.vscode.boot.java.stereotypes.StereotypesIndexer;
2830
import org.springframework.ide.vscode.boot.java.utils.RestrictedDefaultSymbolProvider;
2931

3032
@Configuration(proxyBeanMethods = false)
3133
public class SpringSymbolIndexerConfig {
3234

3335
@Bean
34-
AnnotationHierarchyAwareLookup<SymbolProvider> symbolProviders(IndexCache cache, DataRepositoryAotMetadataService repositoryMetadataService) {
36+
AnnotationHierarchyAwareLookup<SymbolProvider> symbolProviders(IndexCache cache, DataRepositoryAotMetadataService repositoryMetadataService, StereotypeCatalogRegistry stereotypeCatalogRegistry) {
3537
AnnotationHierarchyAwareLookup<SymbolProvider> providers = new AnnotationHierarchyAwareLookup<>();
3638

3739
RequestMappingSymbolProvider requestMappingSymbolProvider = new RequestMappingSymbolProvider();
@@ -82,6 +84,8 @@ AnnotationHierarchyAwareLookup<SymbolProvider> symbolProviders(IndexCache cache,
8284

8385
providers.put(Annotations.FEIGN_CLIENT, new FeignClientSymbolProvider());
8486
providers.put(Annotations.HTTP_EXCHANGE, new HttpExchangeSymbolProvider());
87+
88+
providers.put(Annotations.JMOLECULES_STEREOTYPE, new StereotypesIndexer(stereotypeCatalogRegistry));
8589

8690
return providers;
8791
}

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ public class Annotations {
130130
Annotations.RESOURCE_JAKARTA, Annotations.INJECT_JAKARTA, Annotations.NAMED_JAKARTA,
131131
Annotations.RESOURCE_JAVAX, Annotations.INJECT_JAVAX, Annotations.NAMED_JAVAX
132132
);
133-
133+
134+
public static final String JMOLECULES_STEREOTYPE = "org.jmolecules.stereotype.Stereotype";
134135

135136
}

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/SymbolProvider.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import org.eclipse.jdt.core.dom.Annotation;
1616
import org.eclipse.jdt.core.dom.ITypeBinding;
1717
import org.eclipse.jdt.core.dom.MethodDeclaration;
18+
import org.eclipse.jdt.core.dom.PackageDeclaration;
1819
import org.eclipse.jdt.core.dom.TypeDeclaration;
1920
import org.springframework.ide.vscode.boot.java.utils.SpringIndexerJavaContext;
2021
import org.springframework.ide.vscode.commons.util.text.TextDocument;
@@ -28,5 +29,6 @@ public interface SymbolProvider {
2829
default void addSymbols(Annotation node, ITypeBinding typeBinding, Collection<ITypeBinding> metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) {};
2930
default void addSymbols(TypeDeclaration typeDeclaration, SpringIndexerJavaContext context, TextDocument doc) {};
3031
default void addSymbols(MethodDeclaration methodDeclaration, SpringIndexerJavaContext context, TextDocument doc) {};
32+
default void addSymbols(PackageDeclaration packageDeclaration, SpringIndexerJavaContext context, TextDocument doc) {};
3133

3234
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Broadcom, Inc.
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v1.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-v10.html
7+
*
8+
* Contributors:
9+
* Broadcom, Inc. - initial API and implementation
10+
*******************************************************************************/
11+
package org.springframework.ide.vscode.boot.java.stereotypes;
12+
13+
import java.util.ArrayList;
14+
import java.util.Collection;
15+
import java.util.List;
16+
import java.util.Optional;
17+
import java.util.TreeSet;
18+
19+
import org.jmolecules.stereotype.api.Stereotype;
20+
import org.jmolecules.stereotype.api.StereotypeFactory;
21+
import org.jmolecules.stereotype.api.Stereotypes;
22+
import org.jmolecules.stereotype.catalog.StereotypeDefinition.Assignment;
23+
import org.jmolecules.stereotype.catalog.StereotypeDefinition.Assignment.Type;
24+
import org.jmolecules.stereotype.catalog.StereotypeMatcher;
25+
import org.jmolecules.stereotype.catalog.support.AbstractStereotypeCatalog;
26+
import org.jmolecules.stereotype.support.StringBasedStereotype;
27+
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
28+
29+
public class IndexBasedStereotypeFactory implements StereotypeFactory<StereotypePackageElement, StereotypeClassElement, StereotypeMethodElement> {
30+
31+
private final AbstractStereotypeCatalog catalog;
32+
private final SpringMetamodelIndex springIndex;
33+
34+
private static final StereotypeMatcher<StereotypeClassElement, StereotypeAnnotatedElement> STEREOTYPE_MATCHER = StereotypeMatcher
35+
.<StereotypeClassElement, StereotypeAnnotatedElement> isAnnotatedWith((element, fqn) -> isAnnotated(element, fqn))
36+
.orImplements((type, fqn) -> doesImplement(type, fqn));
37+
38+
39+
public IndexBasedStereotypeFactory(AbstractStereotypeCatalog catalog, SpringMetamodelIndex springIndex) {
40+
this.catalog = catalog;
41+
this.springIndex = springIndex;
42+
}
43+
44+
public void registerStereotypeDefinitions() {
45+
springIndex.getNodesOfType(StereotypeDefinitionElement.class).stream()
46+
.forEach(element -> registerStereotype(element));
47+
}
48+
49+
private void registerStereotype(StereotypeDefinitionElement element) {
50+
var stereotype = StringBasedStereotype.of(element.getType());
51+
Type assignment = element.getAssignment();
52+
53+
catalog.getOrRegister(stereotype, () -> Assignment.of(element.getType(), assignment))
54+
.getStereotype();
55+
}
56+
57+
@Override
58+
public Stereotypes fromPackage(StereotypePackageElement pkg) {
59+
return new Stereotypes(fromAnnotatedElement(pkg));
60+
}
61+
62+
@Override
63+
public Stereotypes fromType(StereotypeClassElement type) {
64+
return new Stereotypes(fromTypeInternal(type))
65+
.and(fromPackage(findPackageFor(type)));
66+
}
67+
68+
@Override
69+
public Stereotypes fromMethod(StereotypeMethodElement method) {
70+
return new Stereotypes(List.of());
71+
}
72+
73+
private <T extends StereotypeAnnotatedElement> Collection<Stereotype> fromAnnotatedElement(StereotypeAnnotatedElement element) {
74+
75+
var result = new ArrayList<Stereotype>();
76+
77+
if (element != null) {
78+
result.addAll(catalog.getAnnotationBasedStereotypes(element, STEREOTYPE_MATCHER));
79+
80+
// for (Annotation annotation : element.getAnnotations()) {
81+
// for (Class<?> type : fromAnnotation(annotation)) {
82+
// result.add(registerStereotypeDefinition(type, Type.IS_ANNOTATED));
83+
// }
84+
// }
85+
}
86+
87+
return result;
88+
}
89+
90+
private Collection<Stereotype> fromTypeInternal(StereotypeClassElement type) {
91+
92+
var result = new TreeSet<Stereotype>();
93+
94+
result.addAll(catalog.getTypeBasedStereotypes(type, STEREOTYPE_MATCHER));
95+
96+
// if (type.isAnnotation()) {
97+
//
98+
// result.addAll(fromAnnotatedElement(type));
99+
//
100+
// return result;
101+
// }
102+
//
103+
// var candidates = type.getInterfaces();
104+
//
105+
// for (Class<?> candidate : candidates) {
106+
// if (candidate.getAnnotation(STEREOTYPE_ANNOTATION) != null) {
107+
// result.add(registerStereotypeDefinition(candidate, Type.IS_ANNOTATED));
108+
// }
109+
// }
110+
//
111+
// for (Class<?> candidate : candidates) {
112+
// result.addAll(fromTypeInternal(candidate));
113+
// }
114+
//
115+
// var superType = type.getSuperclass();
116+
//
117+
// if (superType != null && !Object.class.equals(superType)) {
118+
// result.addAll(fromTypeInternal(type.getSuperclass()));
119+
// }
120+
//
121+
// if (!type.isAnnotation()) {
122+
result.addAll(fromAnnotatedElement(type));
123+
// }
124+
125+
return result;
126+
}
127+
128+
129+
130+
private static boolean isAnnotated(StereotypeAnnotatedElement element, String fqn) {
131+
return element.getAnnotationTypes().stream()
132+
.filter(it -> !it.startsWith("java"))
133+
.anyMatch(it -> it.equals(fqn) /* || isAnnotated(it, fqn)*/ );
134+
}
135+
136+
private static boolean doesImplement(StereotypeClassElement type, String fqn) {
137+
return type.doesImplement(fqn);
138+
}
139+
140+
private StereotypePackageElement findPackageFor(StereotypeClassElement type) {
141+
String packageName = type.getType().substring(0, type.getType().lastIndexOf('.'));
142+
143+
Optional<StereotypePackageElement> result = springIndex.getNodesOfType(StereotypePackageElement.class).stream()
144+
.filter(pkg -> pkg.getPackageName().equals(packageName))
145+
.findFirst();
146+
147+
return result.isPresent() ? result.get() : null;
148+
}
149+
150+
151+
152+
153+
154+
155+
156+
157+
158+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Broadcom, Inc.
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v1.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-v10.html
7+
*
8+
* Contributors:
9+
* Broadcom, Inc. - initial API and implementation
10+
*******************************************************************************/
11+
package org.springframework.ide.vscode.boot.java.stereotypes;
12+
13+
import java.io.File;
14+
import java.net.URI;
15+
import java.net.URL;
16+
import java.util.ArrayList;
17+
import java.util.Collection;
18+
import java.util.List;
19+
import java.util.jar.JarFile;
20+
import java.util.stream.Stream;
21+
import java.util.zip.ZipEntry;
22+
23+
import org.jmolecules.stereotype.catalog.support.CatalogSource;
24+
import org.springframework.ide.vscode.commons.java.IClasspath;
25+
import org.springframework.ide.vscode.commons.java.IJavaProject;
26+
import org.springframework.ide.vscode.commons.protocol.java.Classpath;
27+
import org.springframework.ide.vscode.commons.protocol.java.Classpath.CPE;
28+
29+
public class ProjectBasedCatalogSource implements CatalogSource {
30+
31+
private final IJavaProject project;
32+
33+
public ProjectBasedCatalogSource(IJavaProject project) {
34+
this.project = project;
35+
}
36+
37+
@Override
38+
public Stream<URL> getSources() {
39+
List<URL> result = new ArrayList<>();
40+
41+
try {
42+
IClasspath classpath = project.getClasspath();
43+
Collection<CPE> entries = classpath.getClasspathEntries();
44+
45+
for (CPE cpe : entries) {
46+
if (Classpath.ENTRY_KIND_SOURCE.equals(cpe.getKind()) && !cpe.isTest() && !cpe.isSystem()) {
47+
String path = cpe.getPath();
48+
49+
File stereotypes = new File(path, CatalogSource.DEFAULT_STEREOTYPE_LOCATION);
50+
if (stereotypes.exists() && stereotypes.isFile()) {
51+
result.add(stereotypes.toURI().toURL());
52+
}
53+
54+
File groups = new File(path, CatalogSource.DEFAULT_GROUP_LOCATION);
55+
if (groups.exists() && groups.isFile()) {
56+
result.add(groups.toURI().toURL());
57+
}
58+
}
59+
60+
if (Classpath.ENTRY_KIND_BINARY.equals(cpe.getKind()) && !cpe.isTest() && !cpe.isSystem()) {
61+
String libPath = cpe.getPath();
62+
63+
try (JarFile jarFile = new JarFile(libPath)) {
64+
ZipEntry stereotypeEntry = jarFile.getEntry(CatalogSource.DEFAULT_STEREOTYPE_LOCATION);
65+
if (stereotypeEntry != null) {
66+
URI uri = new URI("jar:" + new File(libPath).toURI().toString() + "!/" + CatalogSource.DEFAULT_STEREOTYPE_LOCATION);
67+
result.add(uri.toURL());
68+
}
69+
70+
ZipEntry groupEntry = jarFile.getEntry(CatalogSource.DEFAULT_GROUP_LOCATION);
71+
if (groupEntry != null) {
72+
URI uri = new URI("jar:" + new File(libPath).toURI().toString() + "!/" + CatalogSource.DEFAULT_GROUP_LOCATION);
73+
result.add(uri.toURL());
74+
}
75+
}
76+
}
77+
}
78+
} catch (Exception e) {
79+
}
80+
81+
return result.stream();
82+
}
83+
84+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Broadcom, Inc.
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v1.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-v10.html
7+
*
8+
* Contributors:
9+
* Broadcom, Inc. - initial API and implementation
10+
*******************************************************************************/
11+
package org.springframework.ide.vscode.boot.java.stereotypes;
12+
13+
import java.util.List;
14+
15+
public interface StereotypeAnnotatedElement {
16+
17+
public List<String> getAnnotationTypes();
18+
19+
}

0 commit comments

Comments
 (0)