diff --git a/headless-services/spring-boot-language-server/pom.xml b/headless-services/spring-boot-language-server/pom.xml index 211f5aa57..01cb26184 100644 --- a/headless-services/spring-boot-language-server/pom.xml +++ b/headless-services/spring-boot-language-server/pom.xml @@ -49,6 +49,18 @@ false + + + jmolecules-snapshots + https://s01.oss.sonatype.org/content/repositories/snapshots + + true + + + false + + + @@ -114,6 +126,7 @@ commons-rewrite ${dependencies.version} + org.eclipse.jdt org.eclipse.jdt.core @@ -138,6 +151,12 @@ org.apache.commons commons-text + + + org.jmolecules.integrations + jmolecules-stereotype + 0.27.0-STEREOTYPE-SNAPSHOT + 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 b56b6b0f1..e569e9fdf 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 @@ -76,6 +76,7 @@ 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.spel.SpelDefinitionProvider; +import org.springframework.ide.vscode.boot.java.stereotypes.StereotypeCatalogRegistry; import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; import org.springframework.ide.vscode.boot.java.value.ValueDefinitionProvider; import org.springframework.ide.vscode.boot.jdt.ls.JavaProjectsService; @@ -434,6 +435,11 @@ public class BootLanguageServerBootApp { return new DataRepositoryAotMetadataService(); } + @Bean + StereotypeCatalogRegistry stereotypeCatalogRegistry() { + return new StereotypeCatalogRegistry(); + } + @Bean ResponseModifier responseModifier() { return new ResponseModifier(); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/SpringSymbolIndexerConfig.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/SpringSymbolIndexerConfig.java index f3f275409..a6ba0640c 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/SpringSymbolIndexerConfig.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/SpringSymbolIndexerConfig.java @@ -24,13 +24,15 @@ import org.springframework.ide.vscode.boot.java.data.DataRepositorySymbolProvide import org.springframework.ide.vscode.boot.java.events.EventListenerSymbolProvider; import org.springframework.ide.vscode.boot.java.handlers.SymbolProvider; import org.springframework.ide.vscode.boot.java.requestmapping.RequestMappingSymbolProvider; +import org.springframework.ide.vscode.boot.java.stereotypes.StereotypeCatalogRegistry; +import org.springframework.ide.vscode.boot.java.stereotypes.StereotypesIndexer; import org.springframework.ide.vscode.boot.java.utils.RestrictedDefaultSymbolProvider; @Configuration(proxyBeanMethods = false) public class SpringSymbolIndexerConfig { @Bean - AnnotationHierarchyAwareLookup symbolProviders(IndexCache cache, DataRepositoryAotMetadataService repositoryMetadataService) { + AnnotationHierarchyAwareLookup symbolProviders(IndexCache cache, DataRepositoryAotMetadataService repositoryMetadataService, StereotypeCatalogRegistry stereotypeCatalogRegistry) { AnnotationHierarchyAwareLookup providers = new AnnotationHierarchyAwareLookup<>(); RequestMappingSymbolProvider requestMappingSymbolProvider = new RequestMappingSymbolProvider(); @@ -80,6 +82,8 @@ public class SpringSymbolIndexerConfig { providers.put(Annotations.EVENT_LISTENER, eventListenerSymbolProvider); providers.put(Annotations.FEIGN_CLIENT, new FeignClientSymbolProvider()); + + providers.put(Annotations.JMOLECULES_STEREOTYPE, new StereotypesIndexer(stereotypeCatalogRegistry)); return providers; } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java index ca07edcb0..4939c19bb 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java @@ -120,6 +120,7 @@ public class Annotations { Annotations.RESOURCE_JAKARTA, Annotations.INJECT_JAKARTA, Annotations.NAMED_JAKARTA, Annotations.RESOURCE_JAVAX, Annotations.INJECT_JAVAX, Annotations.NAMED_JAVAX ); - + + public static final String JMOLECULES_STEREOTYPE = "org.jmolecules.stereotype.Stereotype"; } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/SymbolProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/SymbolProvider.java index 4cb35e0e6..cdfaad503 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/SymbolProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/SymbolProvider.java @@ -15,6 +15,7 @@ import java.util.Collection; import org.eclipse.jdt.core.dom.Annotation; import org.eclipse.jdt.core.dom.ITypeBinding; import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.PackageDeclaration; import org.eclipse.jdt.core.dom.TypeDeclaration; import org.springframework.ide.vscode.boot.java.utils.SpringIndexerJavaContext; import org.springframework.ide.vscode.commons.util.text.TextDocument; @@ -28,5 +29,6 @@ public interface SymbolProvider { default void addSymbols(Annotation node, ITypeBinding typeBinding, Collection metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) {}; default void addSymbols(TypeDeclaration typeDeclaration, SpringIndexerJavaContext context, TextDocument doc) {}; default void addSymbols(MethodDeclaration methodDeclaration, SpringIndexerJavaContext context, TextDocument doc) {}; + default void addSymbols(PackageDeclaration packageDeclaration, SpringIndexerJavaContext context, TextDocument doc) {}; } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/IndexBasedStereotypeFactory.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/IndexBasedStereotypeFactory.java new file mode 100644 index 000000000..acf1b9277 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/IndexBasedStereotypeFactory.java @@ -0,0 +1,158 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.stereotypes; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.TreeSet; + +import org.jmolecules.stereotype.api.Stereotype; +import org.jmolecules.stereotype.api.StereotypeFactory; +import org.jmolecules.stereotype.api.Stereotypes; +import org.jmolecules.stereotype.catalog.StereotypeDefinition.Assignment; +import org.jmolecules.stereotype.catalog.StereotypeDefinition.Assignment.Type; +import org.jmolecules.stereotype.catalog.StereotypeMatcher; +import org.jmolecules.stereotype.catalog.support.AbstractStereotypeCatalog; +import org.jmolecules.stereotype.support.StringBasedStereotype; +import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; + +public class IndexBasedStereotypeFactory implements StereotypeFactory { + + private final AbstractStereotypeCatalog catalog; + private final SpringMetamodelIndex springIndex; + + private static final StereotypeMatcher STEREOTYPE_MATCHER = StereotypeMatcher + . isAnnotatedWith((element, fqn) -> isAnnotated(element, fqn)) + .orImplements((type, fqn) -> doesImplement(type, fqn)); + + + public IndexBasedStereotypeFactory(AbstractStereotypeCatalog catalog, SpringMetamodelIndex springIndex) { + this.catalog = catalog; + this.springIndex = springIndex; + } + + public void registerStereotypeDefinitions() { + springIndex.getNodesOfType(StereotypeDefinitionElement.class).stream() + .forEach(element -> registerStereotype(element)); + } + + private void registerStereotype(StereotypeDefinitionElement element) { + var stereotype = StringBasedStereotype.of(element.getType()); + Type assignment = element.getAssignment(); + + catalog.getOrRegister(stereotype, () -> Assignment.of(element.getType(), assignment)) + .getStereotype(); + } + + @Override + public Stereotypes fromPackage(StereotypePackageElement pkg) { + return new Stereotypes(fromAnnotatedElement(pkg)); + } + + @Override + public Stereotypes fromType(StereotypeClassElement type) { + return new Stereotypes(fromTypeInternal(type)) + .and(fromPackage(findPackageFor(type))); + } + + @Override + public Stereotypes fromMethod(StereotypeMethodElement method) { + return new Stereotypes(List.of()); + } + + private Collection fromAnnotatedElement(StereotypeAnnotatedElement element) { + + var result = new ArrayList(); + + if (element != null) { + result.addAll(catalog.getAnnotationBasedStereotypes(element, STEREOTYPE_MATCHER)); + + // for (Annotation annotation : element.getAnnotations()) { + // for (Class type : fromAnnotation(annotation)) { + // result.add(registerStereotypeDefinition(type, Type.IS_ANNOTATED)); + // } + // } + } + + return result; + } + + private Collection fromTypeInternal(StereotypeClassElement type) { + + var result = new TreeSet(); + + result.addAll(catalog.getTypeBasedStereotypes(type, STEREOTYPE_MATCHER)); + +// if (type.isAnnotation()) { +// +// result.addAll(fromAnnotatedElement(type)); +// +// return result; +// } +// +// var candidates = type.getInterfaces(); +// +// for (Class candidate : candidates) { +// if (candidate.getAnnotation(STEREOTYPE_ANNOTATION) != null) { +// result.add(registerStereotypeDefinition(candidate, Type.IS_ANNOTATED)); +// } +// } +// +// for (Class candidate : candidates) { +// result.addAll(fromTypeInternal(candidate)); +// } +// +// var superType = type.getSuperclass(); +// +// if (superType != null && !Object.class.equals(superType)) { +// result.addAll(fromTypeInternal(type.getSuperclass())); +// } +// +// if (!type.isAnnotation()) { + result.addAll(fromAnnotatedElement(type)); +// } + + return result; + } + + + + private static boolean isAnnotated(StereotypeAnnotatedElement element, String fqn) { + return element.getAnnotationTypes().stream() + .filter(it -> !it.startsWith("java")) + .anyMatch(it -> it.equals(fqn) /* || isAnnotated(it, fqn)*/ ); + } + + private static boolean doesImplement(StereotypeClassElement type, String fqn) { + return type.doesImplement(fqn); + } + + private StereotypePackageElement findPackageFor(StereotypeClassElement type) { + String packageName = type.getType().substring(0, type.getType().lastIndexOf('.')); + + Optional result = springIndex.getNodesOfType(StereotypePackageElement.class).stream() + .filter(pkg -> pkg.getPackageName().equals(packageName)) + .findFirst(); + + return result.isPresent() ? result.get() : null; + } + + + + + + + + + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/ProjectBasedCatalogSource.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/ProjectBasedCatalogSource.java new file mode 100644 index 000000000..bbce3705a --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/ProjectBasedCatalogSource.java @@ -0,0 +1,84 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.stereotypes; + +import java.io.File; +import java.net.URI; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.jar.JarFile; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; + +import org.jmolecules.stereotype.catalog.support.CatalogSource; +import org.springframework.ide.vscode.commons.java.IClasspath; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.protocol.java.Classpath; +import org.springframework.ide.vscode.commons.protocol.java.Classpath.CPE; + +public class ProjectBasedCatalogSource implements CatalogSource { + + private final IJavaProject project; + + public ProjectBasedCatalogSource(IJavaProject project) { + this.project = project; + } + + @Override + public Stream getSources() { + List result = new ArrayList<>(); + + try { + IClasspath classpath = project.getClasspath(); + Collection entries = classpath.getClasspathEntries(); + + for (CPE cpe : entries) { + if (Classpath.ENTRY_KIND_SOURCE.equals(cpe.getKind()) && !cpe.isTest() && !cpe.isSystem()) { + String path = cpe.getPath(); + + File stereotypes = new File(path, CatalogSource.DEFAULT_STEREOTYPE_LOCATION); + if (stereotypes.exists() && stereotypes.isFile()) { + result.add(stereotypes.toURI().toURL()); + } + + File groups = new File(path, CatalogSource.DEFAULT_GROUP_LOCATION); + if (groups.exists() && groups.isFile()) { + result.add(groups.toURI().toURL()); + } + } + + if (Classpath.ENTRY_KIND_BINARY.equals(cpe.getKind()) && !cpe.isTest() && !cpe.isSystem()) { + String libPath = cpe.getPath(); + + try (JarFile jarFile = new JarFile(libPath)) { + ZipEntry stereotypeEntry = jarFile.getEntry(CatalogSource.DEFAULT_STEREOTYPE_LOCATION); + if (stereotypeEntry != null) { + URI uri = new URI("jar:" + new File(libPath).toURI().toString() + "!/" + CatalogSource.DEFAULT_STEREOTYPE_LOCATION); + result.add(uri.toURL()); + } + + ZipEntry groupEntry = jarFile.getEntry(CatalogSource.DEFAULT_GROUP_LOCATION); + if (groupEntry != null) { + URI uri = new URI("jar:" + new File(libPath).toURI().toString() + "!/" + CatalogSource.DEFAULT_GROUP_LOCATION); + result.add(uri.toURL()); + } + } + } + } + } catch (Exception e) { + } + + return result.stream(); + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/StereotypeAnnotatedElement.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/StereotypeAnnotatedElement.java new file mode 100644 index 000000000..657eb9e2e --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/StereotypeAnnotatedElement.java @@ -0,0 +1,19 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.stereotypes; + +import java.util.List; + +public interface StereotypeAnnotatedElement { + + public List getAnnotationTypes(); + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/StereotypeCatalogRegistry.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/StereotypeCatalogRegistry.java new file mode 100644 index 000000000..15142ed55 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/StereotypeCatalogRegistry.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.stereotypes; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.jmolecules.stereotype.catalog.support.AbstractStereotypeCatalog; +import org.jmolecules.stereotype.catalog.support.JsonPathStereotypeCatalog; +import org.springframework.ide.vscode.commons.java.IJavaProject; + +public class StereotypeCatalogRegistry { + + private final Map catalogs; + + public StereotypeCatalogRegistry() { + this.catalogs = new ConcurrentHashMap<>(); + } + + public AbstractStereotypeCatalog getCatalogOf(IJavaProject project) { + return catalogs.computeIfAbsent(project.getElementName(), (p) -> createCatalog(project)); + } + + private AbstractStereotypeCatalog createCatalog(IJavaProject project) { + var source = new ProjectBasedCatalogSource(project); + return new JsonPathStereotypeCatalog(source); + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/StereotypeClassElement.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/StereotypeClassElement.java new file mode 100644 index 000000000..80d72bde6 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/StereotypeClassElement.java @@ -0,0 +1,48 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.stereotypes; + +import java.util.List; +import java.util.Set; + +import org.springframework.ide.vscode.commons.protocol.spring.AbstractSpringIndexElement; + +public class StereotypeClassElement extends AbstractSpringIndexElement implements StereotypeAnnotatedElement { + + private final String type; + private final Set supertypes; + private List annotationTypes; + + public StereotypeClassElement(String type, Set supertypes, List annotationTypes) { + this.type = type; + this.supertypes = supertypes; + this.annotationTypes = annotationTypes; + } + + public String getType() { + return type; + } + + public boolean doesImplement(String fqn) { + if (type.equals(fqn)) { + return true; + } + + // check supertypes + return supertypes.contains(fqn); + } + + @Override + public List getAnnotationTypes() { + return annotationTypes; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/StereotypeDefinitionElement.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/StereotypeDefinitionElement.java new file mode 100644 index 000000000..f82ff18c6 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/StereotypeDefinitionElement.java @@ -0,0 +1,33 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.stereotypes; + +import org.jmolecules.stereotype.catalog.StereotypeDefinition.Assignment.Type; +import org.springframework.ide.vscode.commons.protocol.spring.AbstractSpringIndexElement; + +public class StereotypeDefinitionElement extends AbstractSpringIndexElement { + + private final String type; + private final Type assignment; + + public StereotypeDefinitionElement(String type, Type assignment) { + this.type = type; + this.assignment = assignment; + } + + public String getType() { + return this.type; + } + + public Type getAssignment() { + return this.assignment; + } +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/StereotypeMethodElement.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/StereotypeMethodElement.java new file mode 100644 index 000000000..b474da066 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/StereotypeMethodElement.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.stereotypes; + +import org.springframework.ide.vscode.commons.protocol.spring.AbstractSpringIndexElement; + +public class StereotypeMethodElement extends AbstractSpringIndexElement { + + private final String methodName; + + public StereotypeMethodElement(String methodName) { + this.methodName = methodName; + } + + public String getMethodName() { + return methodName; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/StereotypePackageElement.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/StereotypePackageElement.java new file mode 100644 index 000000000..83712f4a8 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/StereotypePackageElement.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.stereotypes; + +import java.util.List; + +import org.springframework.ide.vscode.commons.protocol.spring.AbstractSpringIndexElement; + +public class StereotypePackageElement extends AbstractSpringIndexElement implements StereotypeAnnotatedElement { + + private final String packageName; + private final List annotationTypes; + + public StereotypePackageElement(String packageName, List annotationTypes) { + this.packageName = packageName; + this.annotationTypes = annotationTypes; + } + + public String getPackageName() { + return packageName; + } + + public List getAnnotationTypes() { + return annotationTypes; + } + +} \ No newline at end of file diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/StereotypesIndexer.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/StereotypesIndexer.java new file mode 100644 index 000000000..a61360684 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/stereotypes/StereotypesIndexer.java @@ -0,0 +1,159 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.stereotypes; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.Annotation; +import org.eclipse.jdt.core.dom.AnnotationTypeDeclaration; +import org.eclipse.jdt.core.dom.IAnnotationBinding; +import org.eclipse.jdt.core.dom.IPackageBinding; +import org.eclipse.jdt.core.dom.ITypeBinding; +import org.eclipse.jdt.core.dom.PackageDeclaration; +import org.eclipse.jdt.core.dom.TypeDeclaration; +import org.jmolecules.stereotype.catalog.StereotypeDefinition.Assignment.Type; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.boot.java.Annotations; +import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies; +import org.springframework.ide.vscode.boot.java.beans.CachedBean; +import org.springframework.ide.vscode.boot.java.handlers.SymbolProvider; +import org.springframework.ide.vscode.boot.java.utils.ASTUtils; +import org.springframework.ide.vscode.boot.java.utils.SpringIndexerJavaContext; +import org.springframework.ide.vscode.commons.util.text.TextDocument; + +import com.google.common.collect.Streams; + +/** + * @author Martin Lippert + */ +public class StereotypesIndexer implements SymbolProvider { + + private static final Logger log = LoggerFactory.getLogger(StereotypesIndexer.class); + + private final StereotypeCatalogRegistry stereotypeCatalogRegistry; + + public StereotypesIndexer(StereotypeCatalogRegistry stereotypeCatalogRegistry) { + this.stereotypeCatalogRegistry = stereotypeCatalogRegistry; + } + + @Override + public void addSymbols(Annotation node, ITypeBinding typeBinding, Collection metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) { + ASTNode parent = node.getParent(); + + if (parent instanceof AnnotationTypeDeclaration) { + AnnotationTypeDeclaration annotationType = (AnnotationTypeDeclaration) parent; + ITypeBinding annotationBinding = annotationType.resolveBinding(); + + StereotypeDefinitionElement stereotypeDefinitionElement = new StereotypeDefinitionElement(annotationBinding.getQualifiedName(), Type.IS_ANNOTATED); + context.getBeans().add(new CachedBean(context.getDocURI(), stereotypeDefinitionElement)); + } + } + + @Override + public void addSymbols(PackageDeclaration packageDeclaration, SpringIndexerJavaContext context, TextDocument doc) { + if (!context.getDocURI().endsWith("package-info.java")) { + return; + } + + IPackageBinding packageBinding = packageDeclaration.resolveBinding(); + if (packageBinding == null) { + return; + } + + @SuppressWarnings("unchecked") + List annotations = packageDeclaration.annotations(); + + List annotationTypes = annotations.stream() + .map(annotation -> annotation.resolveAnnotationBinding()) + .filter(binding -> binding != null) + .map(binding -> binding.getAnnotationType().getQualifiedName()) + .toList(); + + StereotypePackageElement packageElement = new StereotypePackageElement(packageBinding.getName(), annotationTypes); + context.getBeans().add(new CachedBean(context.getDocURI(), packageElement)); + } + + @Override + public void addSymbols(TypeDeclaration typeDeclaration, SpringIndexerJavaContext context, TextDocument doc) { + ITypeBinding typeBinding = typeDeclaration.resolveBinding(); + if (typeBinding == null) { + return; + } + + // identify stereotype definitions themselves + if (typeDeclaration.isInterface()) { + Collection annotations = ASTUtils.getAnnotations(typeDeclaration); + boolean isStereotypeAnnotated = annotations.stream() + .anyMatch(annotation -> annotation.resolveTypeBinding().getQualifiedName().equals(Annotations.JMOLECULES_STEREOTYPE)); + + if (isStereotypeAnnotated) { + StereotypeDefinitionElement stereotypeDefinitionElement = new StereotypeDefinitionElement(typeBinding.getQualifiedName(), Type.IS_ANNOTATED); + context.getBeans().add(new CachedBean(context.getDocURI(), stereotypeDefinitionElement)); + } + } + + // capture type information element for later tree generation + AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(typeDeclaration); + + String qualifiedName = typeBinding.getQualifiedName(); + Set supertypes = ASTUtils.findSupertypes(typeBinding); + + List superTypeAnnotations = new ArrayList<>(); + + Iterator hierarchyTypesBreadthFirstIterator = ASTUtils.getHierarchyTypesBreadthFirstIterator(typeBinding); + while (hierarchyTypesBreadthFirstIterator.hasNext()) { + ITypeBinding superTypeBinding = hierarchyTypesBreadthFirstIterator.next(); + superTypeAnnotations.addAll(Arrays.asList(superTypeBinding.getAnnotations())); + } + + Collection annotations = ASTUtils.getAnnotations(typeDeclaration); + Stream annotationBindings = annotations.stream() + .map(annotation -> annotation.resolveAnnotationBinding()) + .filter(binding -> binding != null) + .flatMap(binding -> Stream.concat(Stream.of(binding), getMetaAnnotations(annotationHierarchies, binding).stream())) + .filter(binding -> !binding.getAnnotationType().getQualifiedName().startsWith("java")); + + List annotationTypes = Streams.concat(annotationBindings, superTypeAnnotations.stream()) + .distinct() + .map(binding -> binding.getAnnotationType().getQualifiedName()) + .toList(); + + context.getBeans().add(new CachedBean(context.getDocURI(), new StereotypeClassElement(qualifiedName, supertypes, annotationTypes))); + + +// } + } + + private boolean isStereotype(TypeDeclaration typeDeclaration, ITypeBinding binding) { + AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(typeDeclaration); + boolean isStereotype = annotationHierarchies.isAnnotatedWith(binding, Annotations.JMOLECULES_STEREOTYPE); + + return isStereotype; + } + + private Collection getMetaAnnotations(AnnotationHierarchies annotationHierarchies, IAnnotationBinding annotationBinding) { + List result = new ArrayList<>(); + for (Iterator itr = annotationHierarchies.iterator(annotationBinding); itr.hasNext();) { + result.add(itr.next()); + } + + return result; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJava.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJava.java index dc796886e..33ca7ddc6 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJava.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJava.java @@ -46,6 +46,7 @@ import org.eclipse.jdt.core.dom.ITypeBinding; import org.eclipse.jdt.core.dom.MarkerAnnotation; import org.eclipse.jdt.core.dom.MethodDeclaration; import org.eclipse.jdt.core.dom.NormalAnnotation; +import org.eclipse.jdt.core.dom.PackageDeclaration; import org.eclipse.jdt.core.dom.SingleMemberAnnotation; import org.eclipse.jdt.core.dom.TypeDeclaration; import org.eclipse.lsp4j.Diagnostic; @@ -144,7 +145,7 @@ public class SpringIndexerJava implements SpringIndexer { @Override public boolean isInterestedIn(String resource) { - return resource.endsWith(".java") && !resource.endsWith("package-info.java"); + return resource.endsWith(".java"); } @Override @@ -719,6 +720,20 @@ public class SpringIndexerJava implements SpringIndexer { private void scanAST(final SpringIndexerJavaContext context, boolean includeReconcile) { try { context.getCu().accept(new ASTVisitor() { + + @Override + public boolean visit(PackageDeclaration node) { + try { + extractSymbolInformation(node, context); + } + catch (RequiredCompleteAstException e) { + throw e; + } + catch (Exception e) { + log.error("error extracting symbol information in project '" + context.getProject().getElementName() + "' - for docURI '" + context.getDocURI() + "' - on node: " + node.toString(), e); + } + return super.visit(node); + } @Override public boolean visit(TypeDeclaration node) { @@ -893,6 +908,16 @@ public class SpringIndexerJava implements SpringIndexer { } } + private void extractSymbolInformation(PackageDeclaration packageDeclaration, final SpringIndexerJavaContext context) throws Exception { + Collection providers = symbolProviders.getAll(); + if (!providers.isEmpty()) { + TextDocument doc = DocumentUtils.getTempTextDocument(context.getDocURI(), context.getDocRef(), context.getContent()); + for (SymbolProvider provider : providers) { + provider.addSymbols(packageDeclaration, context, doc); + } + } + } + private void extractSymbolInformation(Annotation node, final SpringIndexerJavaContext context) throws Exception { IAnnotationBinding annotationBinding = node.resolveAnnotationBinding(); diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/stereotypes/ProjectBasedCatalogSourceTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/stereotypes/ProjectBasedCatalogSourceTest.java new file mode 100644 index 000000000..2ef0f4000 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/stereotypes/ProjectBasedCatalogSourceTest.java @@ -0,0 +1,96 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.stereotypes; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.net.URL; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.jmolecules.stereotype.catalog.StereotypeGroup; +import org.jmolecules.stereotype.catalog.StereotypeGroups; +import org.jmolecules.stereotype.catalog.support.JsonPathStereotypeCatalog; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.ide.vscode.boot.app.SpringSymbolIndex; +import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest; +import org.springframework.ide.vscode.boot.bootiful.SymbolProviderTestConf; +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.project.harness.BootLanguageServerHarness; +import org.springframework.ide.vscode.project.harness.ProjectsHarness; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +/** + * @author Martin Lippert + */ +@ExtendWith(SpringExtension.class) +@BootLanguageServerTest +@Import(SymbolProviderTestConf.class) +public class ProjectBasedCatalogSourceTest { + + @Autowired private BootLanguageServerHarness harness; + @Autowired private JavaProjectFinder projectFinder; + @Autowired private SpringSymbolIndex indexer; + @Autowired private SpringMetamodelIndex springIndex; + + private File directory; + private IJavaProject project; + + @BeforeEach + public void setup() throws Exception { + harness.intialize(null); + + directory = new File(ProjectsHarness.class.getResource("/test-projects/test-stereotypes-support/").toURI()); + String projectDir = directory.toURI().toString(); + + project = projectFinder.find(new TextDocumentIdentifier(projectDir)).get(); + + CompletableFuture initProject = indexer.waitOperation(); + initProject.get(5, TimeUnit.SECONDS); + } + + @Test + void testCatalogLookupFromSource() throws Exception { + var source = new ProjectBasedCatalogSource(project); + + Stream sources = source.getSources(); + List list = sources.toList(); + + URL url1 = new File(directory, "src/main/resources/META-INF/jmolecules-stereotypes.json").toURI().toURL(); + URL url2 = new File(directory, "src/main/resources/META-INF/jmolecules-stereotype-groups.json").toURI().toURL(); + + assertTrue(list.contains(url1)); + assertTrue(list.contains(url2)); + } + + @Test + void testCatalogLookupFromLibraries() throws Exception { + var source = new ProjectBasedCatalogSource(project); + var catalog = new JsonPathStereotypeCatalog(source); + + StereotypeGroups groups = catalog.getGroups("org.jmolecules.ddd"); + StereotypeGroup primary = groups.getPrimary(); + + assertEquals("Domain-Driven Design", primary.getDisplayName()); + } + +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/stereotypes/StereotypesIndexerTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/stereotypes/StereotypesIndexerTest.java new file mode 100644 index 000000000..5ead05d60 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/stereotypes/StereotypesIndexerTest.java @@ -0,0 +1,239 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.stereotypes; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.jmolecules.stereotype.api.Stereotype; +import org.jmolecules.stereotype.catalog.StereotypeDefinition; +import org.jmolecules.stereotype.tooling.AsciiArtNodeHandler; +import org.jmolecules.stereotype.tooling.ProjectTree; +import org.jmolecules.stereotype.tooling.SimpleLabelProvider; +import org.jmolecules.stereotype.tooling.StructureProvider.SimpleStructureProvider; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.ide.vscode.boot.app.SpringSymbolIndex; +import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest; +import org.springframework.ide.vscode.boot.bootiful.SymbolProviderTestConf; +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.project.harness.BootLanguageServerHarness; +import org.springframework.ide.vscode.project.harness.ProjectsHarness; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +/** + * @author Martin Lippert + */ +@ExtendWith(SpringExtension.class) +@BootLanguageServerTest +@Import(SymbolProviderTestConf.class) +public class StereotypesIndexerTest { + + @Autowired private BootLanguageServerHarness harness; + @Autowired private JavaProjectFinder projectFinder; + @Autowired private SpringSymbolIndex indexer; + @Autowired private SpringMetamodelIndex springIndex; + @Autowired private StereotypeCatalogRegistry stereotypeCatalogRegistry; + + private File directory; + private IJavaProject project; + + @BeforeEach + public void setup() throws Exception { + harness.intialize(null); + + directory = new File(ProjectsHarness.class.getResource("/test-projects/test-stereotypes-support/").toURI()); + String projectDir = directory.toURI().toString(); + + project = projectFinder.find(new TextDocumentIdentifier(projectDir)).get(); + + CompletableFuture initProject = indexer.waitOperation(); + initProject.get(5, TimeUnit.SECONDS); + } + + @Test + void testStuff() throws Exception { + List stereotypeNodes = springIndex.getNodesOfType(StereotypeClassElement.class); + +// assertEquals(1, stereotypeNodes.size()); + } + + @Test + void testFromPackageIndexBasedFactory() { + + var catalog = this.stereotypeCatalogRegistry.getCatalogOf(project); + var factory = new IndexBasedStereotypeFactory(catalog, springIndex); + factory.registerStereotypeDefinitions(); + + StereotypePackageElement packageElement = springIndex.getNodesOfType(StereotypePackageElement.class).stream() + .filter(pkg -> pkg.getPackageName().equals("example")) + .findAny().get(); + + List allStereotypesFound = factory.fromPackage(packageElement).stream().toList(); + assertEquals(1, allStereotypesFound.size()); + + assertEquals("org.jmolecules.architecture.hexagonal.Port", allStereotypesFound.get(0).getIdentifier()); + + StereotypePackageElement subpackageElement = springIndex.getNodesOfType(StereotypePackageElement.class).stream() + .filter(pkg -> pkg.getPackageName().equals("example.application")) + .findAny().get(); + + allStereotypesFound = factory.fromPackage(subpackageElement).stream().toList(); + assertEquals(1, allStereotypesFound.size()); + + assertEquals("org.jmolecules.architecture.hexagonal.Application", allStereotypesFound.get(0).getIdentifier()); + } + + @Test + void testFromClassIndexBasedFactory() { + + var catalog = this.stereotypeCatalogRegistry.getCatalogOf(project); + var factory = new IndexBasedStereotypeFactory(catalog, springIndex); + factory.registerStereotypeDefinitions(); + + StereotypeClassElement myControllerClassElement = springIndex.getNodesOfType(StereotypeClassElement.class).stream() + .filter(type -> type.getType().equals("example.MyController")) + .findAny().get(); + + List myControllerStereotypes = factory.fromType(myControllerClassElement).stream().toList(); + assertEquals(2, myControllerStereotypes.size()); + + assertEquals("org.springframework.stereotype.Controller", myControllerStereotypes.get(0).getIdentifier()); + assertEquals("org.jmolecules.architecture.hexagonal.Port", myControllerStereotypes.get(1).getIdentifier()); + + + StereotypeClassElement somePrimaryPortClassElement = springIndex.getNodesOfType(StereotypeClassElement.class).stream() + .filter(type -> type.getType().equals("example.application.SomePrimaryPort")) + .findAny().get(); + + List somePrimaryPortStereotypes = factory.fromType(somePrimaryPortClassElement).stream().toList(); + assertEquals(2, somePrimaryPortStereotypes.size()); + + assertEquals("org.jmolecules.ddd.ValueObject", somePrimaryPortStereotypes.get(0).getIdentifier()); + assertEquals("org.jmolecules.architecture.hexagonal.Application", somePrimaryPortStereotypes.get(1).getIdentifier()); + } + + @Test + void testSelfDefinedAnnotationStereotype() { + + var catalog = this.stereotypeCatalogRegistry.getCatalogOf(project); + var factory = new IndexBasedStereotypeFactory(catalog, springIndex); + factory.registerStereotypeDefinitions(); + + // stereotype matching + StereotypeClassElement myStereotypeMarkedClass = springIndex.getNodesOfType(StereotypeClassElement.class).stream() + .filter(type -> type.getType().equals("example.application.MyStereotypeMarkedClass")) + .findAny().get(); + + List myStereotypeMarkedClassStereotypes = factory.fromType(myStereotypeMarkedClass).stream().toList(); + assertEquals(2, myStereotypeMarkedClassStereotypes.size()); + + assertEquals("example.application.MyStereotype", myStereotypeMarkedClassStereotypes.get(0).getIdentifier()); + assertEquals("org.jmolecules.architecture.hexagonal.Application", myStereotypeMarkedClassStereotypes.get(1).getIdentifier()); + + // catalog definition + Optional found = catalog.getDefinitions().stream() + .filter(definition -> definition.getStereotype().getIdentifier().equals("example.application.MyStereotype")) + .findAny(); + assertTrue(found.isPresent()); + } + + @Test + void testWithStereotypeInSuperclass() { + + var catalog = this.stereotypeCatalogRegistry.getCatalogOf(project); + var factory = new IndexBasedStereotypeFactory(catalog, springIndex); + factory.registerStereotypeDefinitions(); + + StereotypeClassElement classElement = springIndex.getNodesOfType(StereotypeClassElement.class).stream() + .filter(type -> type.getType().equals("example.application.SubclassWithSuperclassMarkedWithStereotype")) + .findAny().get(); + + List stereotypes = factory.fromType(classElement).stream().toList(); + assertEquals(2, stereotypes.size()); + + assertEquals("org.jmolecules.ddd.ValueObject", stereotypes.get(0).getIdentifier()); + assertEquals("org.jmolecules.architecture.hexagonal.Application", stereotypes.get(1).getIdentifier()); + } + + @Test + void testTree() { + + var catalog = this.stereotypeCatalogRegistry.getCatalogOf(project); + var factory = new IndexBasedStereotypeFactory(catalog, springIndex); + factory.registerStereotypeDefinitions(); + + var labels = SimpleLabelProvider.forPackage(StereotypePackageElement::getPackageName, StereotypeClassElement::getType, + (StereotypeMethodElement m, StereotypeClassElement __) -> m.getMethodName(), Object::toString); + + var handler = new AsciiArtNodeHandler<>(labels); + + var tree = new ProjectTree<>(factory, catalog, handler) + .withStructureProvider(new SimpleStructureProvider() { + + @Override + public Collection extractPackages(StereotypePackageElement pkg) { + return getAllPackageElements().stream() + .filter(packageElement -> packageElement.getPackageName().startsWith(pkg.getPackageName())) + .toList(); + + // return extractTypes(pkg).stream() + // .map(Class::getPackage) + // .distinct() + // .toList(); + } + + @Override + public Collection extractMethods(StereotypeClassElement type) { + return List.of(); + } + + @Override + public Collection extractTypes(StereotypePackageElement pkg) { + return getAllClassElements().stream() + .filter(element -> element.getType().startsWith(pkg.getPackageName())) + .toList(); + } + }) + // .withGrouper("spring", "org.jmolecules.ddd") + .withGrouper("spring", "org.jmolecules.architecture") + // .withGrouper("org.jmolecules.architecture") + // .withGrouper("spring") + // .withGrouper("org.jmolecules.ddd") + ; + + tree.process(new StereotypePackageElement("example", null)); + + System.out.println(handler.getWriter().toString()); + } + + private List getAllClassElements() { + return springIndex.getNodesOfType(StereotypeClassElement.class); + } + + private List getAllPackageElements() { + return springIndex.getNodesOfType(StereotypePackageElement.class); + } + +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/.mvn/wrapper/maven-wrapper.properties b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..654af46a7 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/mvnw b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/mvnw new file mode 100755 index 000000000..d7c358e5a --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.2 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/mvnw.cmd b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/mvnw.cmd new file mode 100644 index 000000000..6f779cff2 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/mvnw.cmd @@ -0,0 +1,149 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/pom.xml b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/pom.xml new file mode 100644 index 000000000..dfa51d62a --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/pom.xml @@ -0,0 +1,63 @@ + + + 4.0.0 + + com.example + test-stereotypes-support + 0.0.1-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 3.4.4 + + + + + UTF-8 + UTF-8 + 17 + + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.jmolecules + jmolecules-ddd + 2.0.0-STEREOTYPE-SNAPSHOT + + + + org.jmolecules + jmolecules-hexagonal-architecture + 2.0.0-STEREOTYPE-SNAPSHOT + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/MyController.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/MyController.java new file mode 100644 index 000000000..0f0f0e4e2 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/MyController.java @@ -0,0 +1,24 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example; + +import org.springframework.stereotype.Controller; + +/** + * @author Oliver Drotbohm + */ +@Controller +public class MyController {} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/application/ClassWithDirectStereotypeSuper.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/application/ClassWithDirectStereotypeSuper.java new file mode 100644 index 000000000..d0964dca6 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/application/ClassWithDirectStereotypeSuper.java @@ -0,0 +1,5 @@ +package example.application; + +public class ClassWithDirectStereotypeSuper extends DirectStereotypeClass { + +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/application/ClassWithDirectStereotypeSuperinterface.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/application/ClassWithDirectStereotypeSuperinterface.java new file mode 100644 index 000000000..9d9cad5f1 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/application/ClassWithDirectStereotypeSuperinterface.java @@ -0,0 +1,5 @@ +package example.application; + +public class ClassWithDirectStereotypeSuperinterface implements DirectStereotypeInterface { + +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/application/DescribedStereotype.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/application/DescribedStereotype.java new file mode 100644 index 000000000..22b72ba97 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/application/DescribedStereotype.java @@ -0,0 +1,24 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.application; + +/** + * + * @author Oliver Drotbohm + */ +public interface DescribedStereotype { + +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/application/DirectStereotypeClass.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/application/DirectStereotypeClass.java new file mode 100644 index 000000000..ad6965114 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/application/DirectStereotypeClass.java @@ -0,0 +1,7 @@ +package example.application; + +import org.jmolecules.stereotype.Stereotype; + +@Stereotype +public class DirectStereotypeClass { +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/application/DirectStereotypeInterface.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/application/DirectStereotypeInterface.java new file mode 100644 index 000000000..e16c9e815 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/application/DirectStereotypeInterface.java @@ -0,0 +1,7 @@ +package example.application; + +import org.jmolecules.stereotype.Stereotype; + +@Stereotype +public interface DirectStereotypeInterface { +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/application/MainClass.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/application/MainClass.java new file mode 100644 index 000000000..7da68d699 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/application/MainClass.java @@ -0,0 +1,13 @@ +package example.application; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class MainClass { + + public static void main(String[] args) throws Exception { + SpringApplication.run(MainClass.class, args); + } + +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/application/MyStereotype.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/application/MyStereotype.java new file mode 100644 index 000000000..5dca3077c --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/application/MyStereotype.java @@ -0,0 +1,15 @@ +package example.application; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.jmolecules.stereotype.Stereotype; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Stereotype +public @interface MyStereotype { + +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/application/MyStereotypeMarkedClass.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/application/MyStereotypeMarkedClass.java new file mode 100644 index 000000000..92142ecde --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/application/MyStereotypeMarkedClass.java @@ -0,0 +1,6 @@ +package example.application; + +@MyStereotype +public class MyStereotypeMarkedClass { + +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/application/SomePrimaryPort.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/application/SomePrimaryPort.java new file mode 100644 index 000000000..812ce68db --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/application/SomePrimaryPort.java @@ -0,0 +1,26 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.application; + +import org.jmolecules.ddd.annotation.ValueObject; + +/** + * @author Oliver Drotbohm + */ +@ValueObject +public class SomePrimaryPort { + +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/application/SubclassWithSuperclassMarkedWithStereotype.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/application/SubclassWithSuperclassMarkedWithStereotype.java new file mode 100644 index 000000000..df5ff14d5 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/application/SubclassWithSuperclassMarkedWithStereotype.java @@ -0,0 +1,4 @@ +package example.application; + +public class SubclassWithSuperclassMarkedWithStereotype extends SomePrimaryPort { +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/application/package-info.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/application/package-info.java new file mode 100644 index 000000000..56ff7fd30 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/application/package-info.java @@ -0,0 +1,2 @@ +@org.jmolecules.architecture.hexagonal.Application +package example.application; diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/package-info.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/package-info.java new file mode 100644 index 000000000..c4b437835 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/java/example/package-info.java @@ -0,0 +1,2 @@ +@org.jmolecules.architecture.hexagonal.Port +package example; diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/resources/META-INF/jmolecules-stereotype-groups.json b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/resources/META-INF/jmolecules-stereotype-groups.json new file mode 100644 index 000000000..46472aae2 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/resources/META-INF/jmolecules-stereotype-groups.json @@ -0,0 +1,16 @@ +{ + "groups" : [ + { + "ids" : [ "spring.web" ], + "displayName" : "Web" + }, + { + "ids" : [ "spring.web.rest" ], + "displayName" : "REST" + }, + { + "ids" : [ "custom.group" ], + "displayName" : "My Custom Stereotype Group" + } + ] +} \ No newline at end of file diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/resources/META-INF/jmolecules-stereotypes.json b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/resources/META-INF/jmolecules-stereotypes.json new file mode 100644 index 000000000..0f4fce874 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-stereotypes-support/src/main/resources/META-INF/jmolecules-stereotypes.json @@ -0,0 +1,25 @@ +{ + "stereotypes" : [ + { + "id" : "org.springframework.stereotype.Controller", + "assignments" : [ "@org.springframework.stereotype.Controller" ], + "groups" : [ "spring.web" ], + "priority" : 10 + }, + { + "id" : "org.springframework.boot.jackson.JsonMixin", + "assignments" : [ "@org.springframework.boot.jackson.JsonMixin" ], + "displayName" : "JSON Mixin", + "groups" : [ "spring.web.rest" ], + "priority" : 10 + }, + { + "id" : "example.application.DescribedStereotype", + "assignments" : [ "example.application.DescribedStereotype" ], + "displayName" : "Some Described Stereotype", + "groups" : [ "custom.group" ], + "priority" : 5 + } + + ] +} \ No newline at end of file