You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by jd...@apache.org on 2026/07/26 20:33:49 UTC
(grails-intellij-plugin) 29/35: Convert the last of the main-source Kotlin to Java
This is an automated email from the ASF dual-hosted git repository.
jdaugherty pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/grails-intellij-plugin.git
commit 58f5e0ba0788c163d3577aa13f789bdc7f3747a9
Author: James Daugherty <jd...@jdresources.net>
AuthorDate: Sun Jul 26 15:12:09 2026 -0400
Convert the last of the main-source Kotlin to Java
Thirteen files: gorm, feature/linkable, the two lang AST contributors, the
Gradle-aware module builder, and both maven-module files. Everything under
plugin/src/main and pluginModules is now Java apart from the three files that
cannot be rewritten.
Four places needed more than transcription:
- namedArguments.kt used interface delegation (`by delegate`), so
LinkDescriptor now forwards all five NamedArgumentDescriptor members
explicitly and overrides only createReference, as the delegation did.
- transform.kt built methods through MemberBuilder.method(name) { ... },
whose only signature takes a kotlin.jvm.functions.Function1. Decompiling
it showed the body is just `GrLightMethodBuilder(context.manager, name)`
followed by the block, with no hidden defaults, so the Java constructs the
builder directly and needs no Kotlin stdlib.
- GrailsGradleAwareModuleBuilder.getModuleRoot throws the checked
ExecutionException, which linkModule cannot declare because
GrailsModuleBuilder passes it as a java.util.function.Consumer. It wraps
instead; nothing on that path catches ExecutionException, so only the type
reaching the log changes. Kotlin's invariantSeparatorsPathString is
reproduced locally rather than approximated with
FileUtil.toSystemIndependentName.
- MavenWorkspaceConfigurator.getMavenProjectsWithModules returns a
kotlin.sequences.Sequence, which is not Iterable, so the loop drives its
iterator. It resolves from the platform classpath, so the maven module
compiles with no Kotlin plugin at all -- verified by removing it.
GormVersion's two lookup functions become static factories on the enum rather
than a separate holder, since the enum and the functions shared one Kotlin file
and there were only two call sites.
Verified: 186 classes / 987 tests / 0 failures / 0 errors, matching baseline;
rat clean; all 220 descriptor-referenced classes resolve; the built ZIP carries
all six content-module jars and every compileServer.plugin entry resolves in
lib/.
---
.../config/GrailsGradleAwareModuleBuilder.java | 158 ++++++++++++++++++
.../config/GrailsGradleAwareModuleBuilder.kt | 112 -------------
.../plugins/grails/feature/linkable/Linkable.java | 28 ++--
.../linkable/LinkableNamedArgumentsProvider.java | 134 +++++++++++++++
.../linkable/LinkableTransformationSupport.java | 56 +++++++
.../grails/feature/linkable/namedArguments.kt | 85 ----------
.../plugins/grails/feature/linkable/transform.kt | 52 ------
.../jetbrains/plugins/grails/gorm/GormVersion.java | 76 +++++++++
.../jetbrains/plugins/grails/gorm/gormVersion.kt | 87 ----------
.../grails/lang/DirtyCheckableContributor.java | 73 ++++++++
.../grails/lang/DirtyCheckableContributor.kt | 67 --------
.../plugins/grails/lang/GormTraitContributor.java | 184 +++++++++++++++++++++
.../plugins/grails/lang/GormTraitContributor.kt | 132 ---------------
.../domain/GormAstTransformationContributor.java | 3 +-
pluginModules/maven/build.gradle | 1 -
.../maven/GrailsMavenWorkspaceConfigurator.java | 103 ++++++++++++
.../maven/GrailsMavenWorkspaceConfigurator.kt | 90 ----------
.../groovy/grails/maven/MavenNodeProvider.java | 44 +++++
.../groovy/grails/maven/MavenNodeProvider.kt | 36 ----
19 files changed, 845 insertions(+), 676 deletions(-)
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/config/GrailsGradleAwareModuleBuilder.java b/plugin/src/main/java/org/jetbrains/plugins/grails/config/GrailsGradleAwareModuleBuilder.java
new file mode 100644
index 0000000..3271f22
--- /dev/null
+++ b/plugin/src/main/java/org/jetbrains/plugins/grails/config/GrailsGradleAwareModuleBuilder.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2000-2026 JetBrains s.r.o. and contributors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jetbrains.plugins.grails.config;
+
+import com.intellij.execution.ExecutionException;
+import com.intellij.ide.util.projectWizard.ModuleBuilder;
+import com.intellij.notification.NotificationType;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.externalSystem.importing.ImportSpecBuilder;
+import com.intellij.openapi.externalSystem.model.ExternalSystemDataKeys;
+import com.intellij.openapi.externalSystem.model.project.ProjectData;
+import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil;
+import com.intellij.openapi.externalSystem.util.ExternalSystemUtil;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.options.ConfigurationException;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.ModuleRootManager;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.plugins.gradle.service.project.wizard.AbstractGradleModuleBuilder;
+import org.jetbrains.plugins.gradle.settings.DistributionType;
+import org.jetbrains.plugins.gradle.settings.GradleProjectSettings;
+import org.jetbrains.plugins.gradle.util.GradleConstants;
+import org.jetbrains.plugins.grails.GrailsBundle;
+import org.jetbrains.plugins.grails.runner.GrailsConsole;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+public abstract class GrailsGradleAwareModuleBuilder extends ModuleBuilder {
+
+ protected static final Logger LOG = Logger.getInstance(GrailsGradleAwareModuleBuilder.class);
+
+ private @Nullable ProjectData myParentProject;
+ private boolean myIsCreatingNewProject;
+
+ public @Nullable ProjectData getParentProject() {
+ return myParentProject;
+ }
+
+ public void setParentProject(@Nullable ProjectData parentProject) {
+ myParentProject = parentProject;
+ }
+
+ public boolean isCreatingNewProject() {
+ return myIsCreatingNewProject;
+ }
+
+ public void setCreatingNewProject(boolean creatingNewProject) {
+ myIsCreatingNewProject = creatingNewProject;
+ }
+
+ @Override
+ protected void setupModule(@NotNull Module module) throws ConfigurationException {
+ super.setupModule(module);
+ if (myIsCreatingNewProject) {
+ module.getProject().putUserData(ExternalSystemDataKeys.NEWLY_CREATED_PROJECT, Boolean.TRUE);
+ }
+ }
+
+ protected void linkModule(@NotNull Module module) {
+ Project project = module.getProject();
+ VirtualFile root;
+ try {
+ root = getModuleRoot(module);
+ }
+ catch (ExecutionException e) {
+ // linkModule is handed to GrailsModuleBuilder as a java.util.function.Consumer, so the
+ // checked exception cannot be declared here. Nothing on that path catches ExecutionException,
+ // so wrapping changes only the type that reaches the log.
+ throw new RuntimeException(e);
+ }
+
+ Path rootProjectPath;
+ String linkedPath = myParentProject == null ? null : myParentProject.getLinkedExternalProjectPath();
+ if (linkedPath != null) {
+ rootProjectPath = Paths.get(linkedPath);
+ }
+ else if (myIsCreatingNewProject) {
+ rootProjectPath = Paths.get(module.getProject().getBasePath());
+ }
+ else {
+ rootProjectPath = root.toNioPath();
+ }
+
+ boolean fresh = myIsCreatingNewProject || myParentProject == null;
+ ApplicationManager.getApplication().runWriteAction(() -> {
+ try {
+ AbstractGradleModuleBuilder.setupGradleSettingsFile(rootProjectPath, root, project.getName(), module.getName(), fresh, false);
+ LOG.debug("'settings.gradle' file set up for module: " + module);
+ }
+ catch (ConfigurationException e) {
+ LOG.debug(e);
+ GrailsConsole.NOTIFICATION_GROUP
+ .createNotification(GrailsBundle.message("failed.to.create.settings.gradle.notification.title"),
+ e.getMessage() == null ? "" : e.getMessage(),
+ NotificationType.WARNING)
+ .notify(project);
+ }
+ });
+
+ linkGradleProject(fresh, rootProjectPath, project);
+ }
+
+ private void linkGradleProject(boolean fresh, @NotNull Path rootProjectPath, @NotNull Project project) {
+ if (fresh) {
+ GradleProjectSettings gradleSettings = new GradleProjectSettings();
+ gradleSettings.setDistributionType(DistributionType.DEFAULT_WRAPPED);
+ gradleSettings.setExternalProjectPath(invariantSeparatorsPathString(rootProjectPath));
+ gradleSettings.setGradleJvm("#USE_PROJECT_JDK");
+
+ ExternalSystemApiUtil.getSettings(project, GradleConstants.SYSTEM_ID).linkProject(gradleSettings);
+ LOG.debug("Project '" + project + "' linked with " + gradleSettings);
+ }
+
+ if (!myIsCreatingNewProject) {
+ FileDocumentManager.getInstance().saveAllDocuments();
+ LOG.debug("Starting refreshing the project: " + rootProjectPath);
+ }
+
+ ExternalSystemUtil.refreshProject(invariantSeparatorsPathString(rootProjectPath),
+ new ImportSpecBuilder(project, GradleConstants.SYSTEM_ID));
+ }
+
+ /**
+ * Gradle wants forward slashes in linked project paths regardless of platform. Stands in for
+ * Kotlin's {@code Path.invariantSeparatorsPathString}.
+ */
+ private static @NotNull String invariantSeparatorsPathString(@NotNull Path path) {
+ String separator = path.getFileSystem().getSeparator();
+ String asString = path.toString();
+ return "/".equals(separator) ? asString : asString.replace(separator, "/");
+ }
+
+ protected @NotNull VirtualFile getModuleRoot(@NotNull Module module) throws ExecutionException {
+ VirtualFile[] roots = ModuleRootManager.getInstance(module).getContentRoots();
+ if (roots.length == 1) {
+ return roots[0];
+ }
+ throw new ExecutionException(GrailsBundle.message("dialog.message.no.module.root"));
+ }
+}
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/config/GrailsGradleAwareModuleBuilder.kt b/plugin/src/main/java/org/jetbrains/plugins/grails/config/GrailsGradleAwareModuleBuilder.kt
deleted file mode 100644
index dcc5fcf..0000000
--- a/plugin/src/main/java/org/jetbrains/plugins/grails/config/GrailsGradleAwareModuleBuilder.kt
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright 2000-2026 JetBrains s.r.o. and contributors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.jetbrains.plugins.grails.config
-
-import com.intellij.execution.ExecutionException
-import com.intellij.ide.util.projectWizard.ModuleBuilder
-import com.intellij.notification.NotificationType
-import com.intellij.openapi.application.ApplicationManager
-import com.intellij.openapi.diagnostic.logger
-import com.intellij.openapi.externalSystem.importing.ImportSpecBuilder
-import com.intellij.openapi.externalSystem.model.ExternalSystemDataKeys
-import com.intellij.openapi.externalSystem.model.project.ProjectData
-import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil
-import com.intellij.openapi.externalSystem.util.ExternalSystemUtil
-import com.intellij.openapi.fileEditor.FileDocumentManager
-import com.intellij.openapi.module.Module
-import com.intellij.openapi.options.ConfigurationException
-import com.intellij.openapi.project.Project
-import com.intellij.openapi.roots.ModuleRootManager
-import com.intellij.openapi.vfs.VirtualFile
-import org.jetbrains.plugins.gradle.service.project.wizard.AbstractGradleModuleBuilder.setupGradleSettingsFile
-import org.jetbrains.plugins.gradle.settings.DistributionType.DEFAULT_WRAPPED
-import org.jetbrains.plugins.gradle.settings.GradleProjectSettings
-import org.jetbrains.plugins.gradle.util.GradleConstants
-import org.jetbrains.plugins.grails.GrailsBundle
-import org.jetbrains.plugins.grails.runner.GrailsConsole
-import java.nio.file.Path
-import java.nio.file.Paths
-import kotlin.io.path.invariantSeparatorsPathString
-
-abstract class GrailsGradleAwareModuleBuilder : ModuleBuilder() {
- companion object {
- val LOG = logger<GrailsGradleAwareModuleBuilder>()
- }
-
- var parentProject: ProjectData? = null
- var isCreatingNewProject: Boolean = false
-
- @Throws(ConfigurationException::class)
- override fun setupModule(module: Module) {
- super.setupModule(module)
- if (isCreatingNewProject) {
- module.project.putUserData(ExternalSystemDataKeys.NEWLY_CREATED_PROJECT, java.lang.Boolean.TRUE)
- }
- }
-
- protected fun linkModule(module: Module) {
- val project = module.project
- val root = getModuleRoot(module)
-
- val rootProjectPath: Path = parentProject?.linkedExternalProjectPath?.let { Paths.get(it) }
- ?: (if (isCreatingNewProject) Paths.get(module.project.basePath!!) else root.toNioPath())
-
- val fresh = isCreatingNewProject || parentProject == null
- ApplicationManager.getApplication().runWriteAction {
- try {
- setupGradleSettingsFile(rootProjectPath, root, project.name, module.name, fresh, false)
- LOG.debug("'settings.gradle' file set up for module: $module")
- }
- catch (e: ConfigurationException) {
- LOG.debug(e)
- GrailsConsole.NOTIFICATION_GROUP
- .createNotification(GrailsBundle.message("failed.to.create.settings.gradle.notification.title"), e.message ?: "", NotificationType.WARNING)
- .notify(project)
- }
- }
-
- linkGradleProject(fresh, rootProjectPath, project)
- }
-
- private fun linkGradleProject(fresh: Boolean, rootProjectPath: Path, project: Project) {
- if (fresh) {
- val gradleSettings = GradleProjectSettings()
- gradleSettings.distributionType = DEFAULT_WRAPPED
- gradleSettings.externalProjectPath = rootProjectPath.invariantSeparatorsPathString
- gradleSettings.gradleJvm = "#USE_PROJECT_JDK"
-
- ExternalSystemApiUtil.getSettings(project, GradleConstants.SYSTEM_ID).linkProject(gradleSettings)
- LOG.debug("Project '$project' linked with $gradleSettings")
- }
-
- if (!isCreatingNewProject) {
- FileDocumentManager.getInstance().saveAllDocuments()
- LOG.debug("Starting refreshing the project: $rootProjectPath")
- }
-
- ExternalSystemUtil.refreshProject(rootProjectPath.invariantSeparatorsPathString, ImportSpecBuilder(project, GradleConstants.SYSTEM_ID))
- }
-
- @Throws(ExecutionException::class)
- protected fun getModuleRoot(module: Module): VirtualFile {
- val roots = ModuleRootManager.getInstance(module).contentRoots
- if (roots.size == 1) {
- return roots[0]
- }
- throw ExecutionException(GrailsBundle.message("dialog.message.no.module.root"))
- }
-}
diff --git a/pluginModules/maven/build.gradle b/plugin/src/main/java/org/jetbrains/plugins/grails/feature/linkable/Linkable.java
similarity index 53%
copy from pluginModules/maven/build.gradle
copy to plugin/src/main/java/org/jetbrains/plugins/grails/feature/linkable/Linkable.java
index 7ab37e9..752c74b 100644
--- a/pluginModules/maven/build.gradle
+++ b/plugin/src/main/java/org/jetbrains/plugins/grails/feature/linkable/Linkable.java
@@ -13,18 +13,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-plugins {
- id 'org.apache.grails.intellij.build.intellij-module'
- id 'org.apache.grails.intellij.build.kotlin'
-}
+package org.jetbrains.plugins.grails.feature.linkable;
+
+import com.intellij.openapi.util.Key;
+
+/**
+ * Shared between the transformation that synthesises {@code link(Map)} and the provider that
+ * supplies its named arguments: the marker lets the provider recognise the synthetic method.
+ */
+final class Linkable {
+
+ private Linkable() {
+ }
+
+ static final String LINK_FQN = "grails.rest.Link";
-dependencies {
- intellijPlatform {
- bundledPlugin 'org.jetbrains.idea.maven'
- // java debugger classes moved into the java plugin modules in 2026.2
- bundledPlugin 'com.intellij.java'
- }
+ static final Key<Object> LINK_METHOD_KEY = Key.create("grails.linkable.method.key");
- compileOnly project(':libs-grails-rt')
- compileOnly project(':plugin')
+ static final Object LINK_METHOD_MARKER = new Object();
}
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/feature/linkable/LinkableNamedArgumentsProvider.java b/plugin/src/main/java/org/jetbrains/plugins/grails/feature/linkable/LinkableNamedArgumentsProvider.java
new file mode 100644
index 0000000..5219ed5
--- /dev/null
+++ b/plugin/src/main/java/org/jetbrains/plugins/grails/feature/linkable/LinkableNamedArgumentsProvider.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2000-2026 JetBrains s.r.o. and contributors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jetbrains.plugins.grails.feature.linkable;
+
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.codeInsight.lookup.LookupElementBuilder;
+import com.intellij.patterns.PsiMethodPattern;
+import com.intellij.psi.CommonClassNames;
+import com.intellij.psi.JavaPsiFacade;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiField;
+import com.intellij.psi.PsiPolyVariantReference;
+import com.intellij.psi.PsiPolyVariantReferenceBase;
+import com.intellij.psi.PsiSubstitutor;
+import com.intellij.psi.PsiType;
+import com.intellij.psi.ResolveResult;
+import com.intellij.psi.infos.CandidateInfo;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.plugins.groovy.extensions.GroovyNamedArgumentProvider;
+import org.jetbrains.plugins.groovy.extensions.NamedArgumentDescriptor;
+import org.jetbrains.plugins.groovy.extensions.impl.StringTypeCondition;
+import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElement;
+import org.jetbrains.plugins.groovy.lang.psi.api.GroovyResolveResult;
+import org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrArgumentLabel;
+import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrCall;
+
+import java.util.Map;
+
+public final class LinkableNamedArgumentsProvider extends GroovyNamedArgumentProvider {
+
+ private static final NamedArgumentDescriptor STRING_DESCRIPTOR = new LinkDescriptor(NamedArgumentDescriptor.TYPE_STRING);
+ private static final NamedArgumentDescriptor BOOL_DESCRIPTOR = new LinkDescriptor(NamedArgumentDescriptor.TYPE_BOOL);
+
+ private static final Map<String, NamedArgumentDescriptor> LINK_NAMED_ARGS = Map.of(
+ "rel", STRING_DESCRIPTOR,
+ "href", STRING_DESCRIPTOR,
+ "hreflang", new LinkDescriptor(new StringTypeCondition("java.util.Locale")),
+ "contentType", STRING_DESCRIPTOR,
+ "title", STRING_DESCRIPTOR,
+ "deprecated", BOOL_DESCRIPTOR,
+ "templated", BOOL_DESCRIPTOR
+ );
+
+ // grails.rest.Link#createLink(java.util.Map)
+ private static final PsiMethodPattern LINK_METHOD_PATTERN = new PsiMethodPattern()
+ .withName("createLink")
+ .definedInClass(Linkable.LINK_FQN)
+ .withParameters(CommonClassNames.JAVA_UTIL_MAP);
+
+ @Override
+ public void getNamedArguments(@NotNull GrCall call,
+ @NotNull GroovyResolveResult resolveResult,
+ @Nullable String argumentName,
+ boolean forCompletion,
+ @NotNull Map<String, NamedArgumentDescriptor> result) {
+ PsiElement resolved = resolveResult.getElement();
+ boolean isSynthetic = resolved != null && resolved.getUserData(Linkable.LINK_METHOD_KEY) == Linkable.LINK_METHOD_MARKER;
+ if (isSynthetic || LINK_METHOD_PATTERN.accepts(resolved)) {
+ result.putAll(LINK_NAMED_ARGS);
+ }
+ }
+
+ /**
+ * Delegating wrapper that only replaces reference creation, so each label resolves to the matching
+ * field on {@code grails.rest.Link}. Kotlin expressed this with interface delegation.
+ */
+ private static final class LinkDescriptor implements NamedArgumentDescriptor {
+
+ private final NamedArgumentDescriptor myDelegate;
+
+ private LinkDescriptor(NamedArgumentDescriptor delegate) {
+ myDelegate = delegate;
+ }
+
+ @Override
+ public Priority getPriority() {
+ return myDelegate.getPriority();
+ }
+
+ @Override
+ public boolean checkType(@NotNull PsiType type, @NotNull GroovyPsiElement context) {
+ return myDelegate.checkType(type, context);
+ }
+
+ @Override
+ public @Nullable PsiElement getNavigationElement() {
+ return myDelegate.getNavigationElement();
+ }
+
+ @Override
+ public LookupElement customizeLookupElement(@NotNull LookupElementBuilder builder) {
+ return myDelegate.customizeLookupElement(builder);
+ }
+
+ @Override
+ public @NotNull PsiPolyVariantReference createReference(@NotNull GrArgumentLabel label) {
+ return new LinkReference(label);
+ }
+ }
+
+ private static final class LinkReference extends PsiPolyVariantReferenceBase<GrArgumentLabel> {
+
+ private LinkReference(@NotNull GrArgumentLabel label) {
+ super(label);
+ }
+
+ @Override
+ public ResolveResult @NotNull [] multiResolve(boolean incompleteCode) {
+ String name = getElement().getName();
+ if (name == null) return ResolveResult.EMPTY_ARRAY;
+ PsiClass clazz = JavaPsiFacade.getInstance(getElement().getProject())
+ .findClass(Linkable.LINK_FQN, getElement().getResolveScope());
+ if (clazz == null) return ResolveResult.EMPTY_ARRAY;
+ PsiField field = clazz.findFieldByName(name, false);
+ if (field == null) return ResolveResult.EMPTY_ARRAY;
+ return new ResolveResult[]{new CandidateInfo(field, PsiSubstitutor.EMPTY)};
+ }
+ }
+}
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/feature/linkable/LinkableTransformationSupport.java b/plugin/src/main/java/org/jetbrains/plugins/grails/feature/linkable/LinkableTransformationSupport.java
new file mode 100644
index 0000000..58af73d
--- /dev/null
+++ b/plugin/src/main/java/org/jetbrains/plugins/grails/feature/linkable/LinkableTransformationSupport.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2000-2026 JetBrains s.r.o. and contributors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jetbrains.plugins.grails.feature.linkable;
+
+import com.intellij.psi.CommonClassNames;
+import com.intellij.psi.PsiAnnotation;
+import com.intellij.psi.PsiTypes;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.plugins.groovy.lang.psi.api.auxiliary.modifiers.GrModifierFlags;
+import org.jetbrains.plugins.groovy.lang.psi.impl.synthetic.GrLightMethodBuilder;
+import org.jetbrains.plugins.groovy.transformations.AstTransformationSupport;
+import org.jetbrains.plugins.groovy.transformations.TransformationContext;
+
+public final class LinkableTransformationSupport implements AstTransformationSupport {
+
+ @Override
+ public void applyTransformation(@NotNull TransformationContext context) {
+ addLinkMethods(context, "grails.rest.Linkable");
+ addLinkMethods(context, "grails.rest.Resource");
+ }
+
+ private static void addLinkMethods(@NotNull TransformationContext context, @NotNull String fqn) {
+ PsiAnnotation annotation = context.getAnnotation(fqn);
+ if (annotation == null) return;
+
+ // public void link(Map)
+ GrLightMethodBuilder linkMap = new GrLightMethodBuilder(context.getManager(), "link");
+ linkMap.addModifier(GrModifierFlags.PUBLIC_MASK);
+ linkMap.setReturnType(PsiTypes.voidType());
+ linkMap.addParameter("link", CommonClassNames.JAVA_UTIL_MAP);
+ linkMap.setNavigationElement(annotation);
+ linkMap.putUserData(Linkable.LINK_METHOD_KEY, Linkable.LINK_METHOD_MARKER);
+ context.addMethod(linkMap);
+
+ // public void link(Link)
+ GrLightMethodBuilder linkTyped = new GrLightMethodBuilder(context.getManager(), "link");
+ linkTyped.addModifier(GrModifierFlags.PUBLIC_MASK);
+ linkTyped.setReturnType(PsiTypes.voidType());
+ linkTyped.addParameter("link", Linkable.LINK_FQN);
+ linkTyped.setNavigationElement(annotation);
+ context.addMethod(linkTyped);
+ }
+}
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/feature/linkable/namedArguments.kt b/plugin/src/main/java/org/jetbrains/plugins/grails/feature/linkable/namedArguments.kt
deleted file mode 100644
index d5bde7f..0000000
--- a/plugin/src/main/java/org/jetbrains/plugins/grails/feature/linkable/namedArguments.kt
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright 2000-2026 JetBrains s.r.o. and contributors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.jetbrains.plugins.grails.feature.linkable
-
-import com.intellij.openapi.util.Key
-import com.intellij.patterns.PsiMethodPattern
-import com.intellij.psi.CommonClassNames
-import com.intellij.psi.JavaPsiFacade
-import com.intellij.psi.PsiPolyVariantReferenceBase
-import com.intellij.psi.PsiSubstitutor
-import com.intellij.psi.ResolveResult
-import com.intellij.psi.infos.CandidateInfo
-import org.jetbrains.plugins.groovy.extensions.GroovyNamedArgumentProvider
-import org.jetbrains.plugins.groovy.extensions.NamedArgumentDescriptor
-import org.jetbrains.plugins.groovy.extensions.impl.StringTypeCondition
-import org.jetbrains.plugins.groovy.lang.psi.api.GroovyResolveResult
-import org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrArgumentLabel
-import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrCall
-
-internal const val LINK_FQN = "grails.rest.Link"
-
-internal val LINK_METHOD_KEY = Key.create<Any>("grails.linkable.method.key")
-internal val LINK_METHOD_MARKER = Object()
-
-private val STRING_DESCRIPTOR = LinkDescriptor(NamedArgumentDescriptor.TYPE_STRING)
-private val BOOL_DESCRIPTOR = LinkDescriptor(NamedArgumentDescriptor.TYPE_BOOL)
-private val LINK_NAMED_ARGS = mapOf(
- "rel" to STRING_DESCRIPTOR,
- "href" to STRING_DESCRIPTOR,
- "hreflang" to LinkDescriptor(StringTypeCondition("java.util.Locale")),
- "contentType" to STRING_DESCRIPTOR,
- "title" to STRING_DESCRIPTOR,
- "deprecated" to BOOL_DESCRIPTOR,
- "templated" to BOOL_DESCRIPTOR
-)
-
-// grails.rest.Link#createLink(java.util.Map)
-private val LINK_METHOD_PATTERN = PsiMethodPattern().withName("createLink").definedInClass(
- LINK_FQN
-).withParameters(CommonClassNames.JAVA_UTIL_MAP)
-
-private class LinkDescriptor(delegate: NamedArgumentDescriptor) : NamedArgumentDescriptor by delegate {
-
- override fun createReference(label: GrArgumentLabel) = LinkReference(label)
-}
-
-private class LinkReference(label: GrArgumentLabel) : PsiPolyVariantReferenceBase<GrArgumentLabel>(label) {
-
- override fun multiResolve(incompleteCode: Boolean): Array<out ResolveResult> {
- val name = element.name ?: return emptyArray()
- val clazz = JavaPsiFacade.getInstance(element.project).findClass(LINK_FQN, element.resolveScope) ?: return emptyArray()
- return clazz.findFieldByName(name, false)?.let {
- arrayOf(CandidateInfo(it, PsiSubstitutor.EMPTY))
- } ?: emptyArray()
- }
-}
-
-class LinkableNamedArgumentsProvider : GroovyNamedArgumentProvider() {
-
- override fun getNamedArguments(call: GrCall,
- resolveResult: GroovyResolveResult,
- argumentName: String?,
- forCompletion: Boolean,
- result: MutableMap<String, NamedArgumentDescriptor>) {
- val resolved = resolveResult.element
- if (resolved?.getUserData(LINK_METHOD_KEY) == LINK_METHOD_MARKER || LINK_METHOD_PATTERN.accepts(resolved)) {
- result += LINK_NAMED_ARGS
- }
- }
-}
-
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/feature/linkable/transform.kt b/plugin/src/main/java/org/jetbrains/plugins/grails/feature/linkable/transform.kt
deleted file mode 100644
index 2770063..0000000
--- a/plugin/src/main/java/org/jetbrains/plugins/grails/feature/linkable/transform.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright 2000-2026 JetBrains s.r.o. and contributors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.jetbrains.plugins.grails.feature.linkable
-
-import com.intellij.psi.CommonClassNames
-import com.intellij.psi.PsiTypes
-import org.jetbrains.plugins.groovy.lang.psi.api.auxiliary.modifiers.GrModifierFlags
-import org.jetbrains.plugins.groovy.transformations.AstTransformationSupport
-import org.jetbrains.plugins.groovy.transformations.TransformationContext
-
-class LinkableTransformationSupport : AstTransformationSupport {
-
- override fun applyTransformation(context: TransformationContext) {
- addLinkMethods(context, "grails.rest.Linkable")
- addLinkMethods(context, "grails.rest.Resource")
- }
-}
-
-private fun addLinkMethods(context: TransformationContext, fqn: String) {
- context.getAnnotation(fqn)?.let {
- // public void link(Map)
- context.addMethod(context.memberBuilder.method("link") {
- addModifier(GrModifierFlags.PUBLIC_MASK)
- returnType = PsiTypes.voidType()
- addParameter("link", CommonClassNames.JAVA_UTIL_MAP)
- navigationElement = it
- putUserData(LINK_METHOD_KEY, LINK_METHOD_MARKER)
- })
-
- // public void link(Link)
- context.addMethod(context.memberBuilder.method("link") {
- addModifier(GrModifierFlags.PUBLIC_MASK)
- returnType = PsiTypes.voidType()
- addParameter("link", LINK_FQN)
- navigationElement = it
- })
- }
-}
\ No newline at end of file
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/gorm/GormVersion.java b/plugin/src/main/java/org/jetbrains/plugins/grails/gorm/GormVersion.java
new file mode 100644
index 0000000..86c74a9
--- /dev/null
+++ b/plugin/src/main/java/org/jetbrains/plugins/grails/gorm/GormVersion.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2000-2026 JetBrains s.r.o. and contributors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jetbrains.plugins.grails.gorm;
+
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.module.ModuleUtilCore;
+import com.intellij.openapi.roots.ProjectRootManager;
+import com.intellij.psi.JavaPsiFacade;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.psi.util.CachedValueProvider.Result;
+import com.intellij.psi.util.CachedValuesManager;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.plugins.grails.references.domain.DomainDescriptor;
+
+public enum GormVersion {
+ /**
+ * GormApi compile time injection
+ */
+ BELOW_4,
+ /**
+ * GormEntity trait injection
+ */
+ IS_4,
+ /**
+ * GormEntity trait injection with custom traits for different DBs (Mongo, Cassandra, etc)
+ */
+ IS_5,
+ /**
+ * Custom traits for different DBs which are checked for availability in current context
+ */
+ IS_6;
+
+ public static @Nullable GormVersion forElement(@Nullable PsiElement element) {
+ return element == null ? null : forModule(ModuleUtilCore.findModuleForPsiElement(element));
+ }
+
+ public static @Nullable GormVersion forModule(@Nullable Module module) {
+ if (module == null) return null;
+ return CachedValuesManager.getManager(module.getProject()).getCachedValue(
+ module,
+ () -> Result.create(detect(module), ProjectRootManager.getInstance(module.getProject())));
+ }
+
+ private static @Nullable GormVersion detect(Module module) {
+ JavaPsiFacade facade = JavaPsiFacade.getInstance(module.getProject());
+ GlobalSearchScope scope = GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(module);
+
+ // present since GORM 6
+ if (facade.findClass(GormClassNames.QUERY_CREATOR, scope) != null) return IS_6;
+
+ // present since GORM 5
+ if (facade.findClass(GormClassNames.ENTITY_ANNO, scope) != null) return IS_5;
+
+ // present since GORM 4
+ if (facade.findClass(GormClassNames.ENTITY_TRAIT, scope) != null) return IS_4;
+
+ // GormInstanceApi class is present in all versions, so we check it last
+ if (facade.findClass(DomainDescriptor.GORM_INSTANCE_API_CLASS, scope) != null) return BELOW_4;
+
+ return null;
+ }
+}
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/gorm/gormVersion.kt b/plugin/src/main/java/org/jetbrains/plugins/grails/gorm/gormVersion.kt
deleted file mode 100644
index d856b39..0000000
--- a/plugin/src/main/java/org/jetbrains/plugins/grails/gorm/gormVersion.kt
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright 2000-2026 JetBrains s.r.o. and contributors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.jetbrains.plugins.grails.gorm
-
-import com.intellij.openapi.module.Module
-import com.intellij.openapi.module.ModuleUtilCore
-import com.intellij.openapi.roots.ProjectRootManager
-import com.intellij.psi.JavaPsiFacade
-import com.intellij.psi.PsiElement
-import com.intellij.psi.search.GlobalSearchScope
-import com.intellij.psi.util.CachedValueProvider.Result
-import com.intellij.psi.util.CachedValuesManager
-import org.jetbrains.plugins.grails.gorm.GormClassNames.ENTITY_ANNO
-import org.jetbrains.plugins.grails.gorm.GormClassNames.ENTITY_TRAIT
-import org.jetbrains.plugins.grails.gorm.GormClassNames.QUERY_CREATOR
-import org.jetbrains.plugins.grails.references.domain.DomainDescriptor.GORM_INSTANCE_API_CLASS
-
-enum class GormVersion {
- /**
- * GormApi compile time injection
- */
- BELOW_4,
- /**
- * GormEntity trait injection
- */
- IS_4,
- /**
- * GormEntity trait injection with custom traits for different DBs (Mongo, Cassandra, etc)
- */
- IS_5,
- /**
- * Custom traits for different DBs which are checked for availability in current context
- */
- IS_6
-}
-
-fun getGormVersion(element: PsiElement?): GormVersion? = element?.let {
- getGormVersion(ModuleUtilCore.findModuleForPsiElement(it))
-}
-
-fun getGormVersion(module: Module?): GormVersion? = module?.let {
- CachedValuesManager.getManager(it.project).getCachedValue(it) {
- Result.create(doGetGormVersion(it), ProjectRootManager.getInstance(it.project))
- }
-}
-
-private fun doGetGormVersion(module: Module): GormVersion? {
- val project = module.project
- val facade = JavaPsiFacade.getInstance(project)
- val scope = GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(module)
-
- // present since GORM 6
- facade.findClass(QUERY_CREATOR, scope)?.let {
- return GormVersion.IS_6
- }
-
- // present since GORM 5
- facade.findClass(ENTITY_ANNO, scope)?.let {
- return GormVersion.IS_5
- }
-
- // present since GORM 4
- facade.findClass(ENTITY_TRAIT, scope)?.let {
- return GormVersion.IS_4
- }
-
- // GormInstanceApi class is present in all versions, so we check it last
- facade.findClass(GORM_INSTANCE_API_CLASS, scope)?.let {
- return GormVersion.BELOW_4
- }
-
- return null
-}
\ No newline at end of file
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/lang/DirtyCheckableContributor.java b/plugin/src/main/java/org/jetbrains/plugins/grails/lang/DirtyCheckableContributor.java
new file mode 100644
index 0000000..2ab7afc
--- /dev/null
+++ b/plugin/src/main/java/org/jetbrains/plugins/grails/lang/DirtyCheckableContributor.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2000-2026 JetBrains s.r.o. and contributors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jetbrains.plugins.grails.lang;
+
+import com.intellij.codeInsight.AnnotationUtil;
+import com.intellij.psi.JavaPsiFacade;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiMethod;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.plugins.grails.util.GrailsArtifact;
+import org.jetbrains.plugins.grails.util.GrailsUtils;
+import org.jetbrains.plugins.groovy.lang.psi.api.auxiliary.modifiers.GrModifierFlags;
+import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrTypeDefinition;
+import org.jetbrains.plugins.groovy.lang.psi.impl.synthetic.GrMethodWrapper;
+import org.jetbrains.plugins.groovy.lang.psi.util.GrTraitUtil;
+import org.jetbrains.plugins.groovy.transformations.AstTransformationSupport;
+import org.jetbrains.plugins.groovy.transformations.TransformationContext;
+
+public final class DirtyCheckableContributor implements AstTransformationSupport {
+
+ public static final String DIRTY_CHECK_FQN = "grails.gorm.dirty.checking.DirtyCheck";
+ public static final String DIRTY_CHECKABLE_FQN = "org.grails.datastore.mapping.dirty.checking.DirtyCheckable";
+ public static final String ORIGIN_INFO = "via @DirtyCheck";
+
+ /**
+ * DirtyCheckable is added in compile time when:
+ *
+ * <ol>
+ * <li>class annotated explicitly -> org.codehaus.groovy.grails.compiler.gorm.DirtyCheckTransformation</li>
+ * <li>class is a GORM domain</li>
+ * </ol>
+ */
+ private static boolean check(@NotNull GrTypeDefinition clazz) {
+ return AnnotationUtil.isAnnotated(clazz, DIRTY_CHECK_FQN, 0)
+ || GrailsUtils.calculateArtifactType(clazz) == GrailsArtifact.DOMAIN;
+ }
+
+ /**
+ * In GORM 5, DirtyCheckable was introduced as a trait.
+ * If it is a trait, then we do not collect its methods here,
+ * because they will be collected in
+ * {@link org.jetbrains.plugins.groovy.transformations.impl.TraitTransformationSupport TraitTransformationSupport}.
+ * If it is a simple interface, then we are dealing with pre GORM 5, where all its methods are added one by one to the target class.
+ */
+ @Override
+ public void applyTransformation(@NotNull TransformationContext context) {
+ GrTypeDefinition clazz = context.getCodeClass();
+ if (!check(clazz)) return;
+ context.addInterface(DIRTY_CHECKABLE_FQN);
+ PsiClass dirtyCheckable = JavaPsiFacade.getInstance(clazz.getProject())
+ .findClass(DIRTY_CHECKABLE_FQN, clazz.getResolveScope());
+ if (dirtyCheckable == null || !dirtyCheckable.isInterface() || GrTraitUtil.isTrait(dirtyCheckable)) return;
+ for (PsiMethod interfaceMethod : dirtyCheckable.getMethods()) {
+ GrMethodWrapper wrapper = GrMethodWrapper.wrap(interfaceMethod);
+ wrapper.getModifierList().removeModifier(GrModifierFlags.ABSTRACT_MASK);
+ wrapper.setOriginInfo(ORIGIN_INFO);
+ context.addMethod(wrapper);
+ }
+ }
+}
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/lang/DirtyCheckableContributor.kt b/plugin/src/main/java/org/jetbrains/plugins/grails/lang/DirtyCheckableContributor.kt
deleted file mode 100644
index deacfad..0000000
--- a/plugin/src/main/java/org/jetbrains/plugins/grails/lang/DirtyCheckableContributor.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright 2000-2026 JetBrains s.r.o. and contributors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.jetbrains.plugins.grails.lang
-
-import com.intellij.codeInsight.AnnotationUtil
-import com.intellij.psi.JavaPsiFacade
-import org.jetbrains.plugins.grails.util.GrailsArtifact
-import org.jetbrains.plugins.grails.util.GrailsUtils
-import org.jetbrains.plugins.groovy.lang.psi.api.auxiliary.modifiers.GrModifierFlags
-import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrTypeDefinition
-import org.jetbrains.plugins.groovy.lang.psi.impl.synthetic.GrMethodWrapper
-import org.jetbrains.plugins.groovy.lang.psi.util.GrTraitUtil
-import org.jetbrains.plugins.groovy.transformations.AstTransformationSupport
-import org.jetbrains.plugins.groovy.transformations.TransformationContext
-
-class DirtyCheckableContributor : AstTransformationSupport {
-
- companion object {
- const val DIRTY_CHECK_FQN: String = "grails.gorm.dirty.checking.DirtyCheck"
- const val DIRTY_CHECKABLE_FQN: String = "org.grails.datastore.mapping.dirty.checking.DirtyCheckable"
- const val ORIGIN_INFO: String = "via @DirtyCheck"
- }
-
- /**
- * DirtyCheckable is added in compile time when:
- *
- * 1. class annotated explicitly -> org.codehaus.groovy.grails.compiler.gorm.DirtyCheckTransformation
- * 2. class is a GORM domain
- */
- private fun check(clazz: GrTypeDefinition) =
- AnnotationUtil.isAnnotated(clazz, DIRTY_CHECK_FQN, 0) || GrailsUtils.calculateArtifactType(clazz) === GrailsArtifact.DOMAIN
-
- /**
- * In GORM 5, DirtyCheckable was introduced as a trait.
- * If it is a trait, then we do not collect its methods here,
- * because they will be collected in
- * [TraitTransformationSupport][org.jetbrains.plugins.groovy.transformations.impl.TraitTransformationSupport].
- * If it is a simple interface, then we are dealing with pre GORM 5, where all its methods are added one by one to the target class.
- */
- override fun applyTransformation(context: TransformationContext) {
- val clazz = context.codeClass
- if (!check(clazz)) return
- context.addInterface(DIRTY_CHECKABLE_FQN)
- val dirtyCheckable = JavaPsiFacade.getInstance(clazz.project).findClass(DIRTY_CHECKABLE_FQN, clazz.resolveScope)
- if (dirtyCheckable == null || !dirtyCheckable.isInterface || GrTraitUtil.isTrait(dirtyCheckable)) return
- for (interfaceMethod in dirtyCheckable.methods) {
- val wrapper = GrMethodWrapper.wrap(interfaceMethod)
- wrapper.modifierList.removeModifier(GrModifierFlags.ABSTRACT_MASK)
- wrapper.originInfo = ORIGIN_INFO
- context.addMethod(wrapper)
- }
- }
-}
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/lang/GormTraitContributor.java b/plugin/src/main/java/org/jetbrains/plugins/grails/lang/GormTraitContributor.java
new file mode 100644
index 0000000..1d1836f
--- /dev/null
+++ b/plugin/src/main/java/org/jetbrains/plugins/grails/lang/GormTraitContributor.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2000-2026 JetBrains s.r.o. and contributors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jetbrains.plugins.grails.lang;
+
+import com.intellij.openapi.roots.ProjectRootManager;
+import com.intellij.psi.JavaPsiFacade;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiField;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.psi.search.searches.ClassInheritorsSearch;
+import com.intellij.psi.util.CachedValueProvider.Result;
+import com.intellij.psi.util.CachedValuesManager;
+import com.intellij.psi.util.PsiModificationTracker;
+import it.unimi.dsi.fastutil.Hash;
+import it.unimi.dsi.fastutil.objects.ObjectOpenCustomHashSet;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.plugins.grails.gorm.GormClassNames;
+import org.jetbrains.plugins.grails.gorm.GormVersion;
+import org.jetbrains.plugins.grails.util.GrailsArtifact;
+import org.jetbrains.plugins.grails.util.GrailsUtils;
+import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrField;
+import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrExpression;
+import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrLiteral;
+import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrTypeDefinition;
+import org.jetbrains.plugins.groovy.lang.psi.impl.GroovyDirectInheritorsSearcher;
+import org.jetbrains.plugins.groovy.lang.psi.util.GrTraitUtil;
+import org.jetbrains.plugins.groovy.transformations.AstTransformationSupport;
+import org.jetbrains.plugins.groovy.transformations.TransformationContext;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+public final class GormTraitContributor implements AstTransformationSupport {
+
+ private static final Map<String, String> MARKER_CLASSES = Map.of(
+ "grails.mongodb.MongoEntity", "com.mongodb.MongoClient",
+ "grails.neo4j.Neo4jEntity", "org.neo4j.driver.v1.Driver"
+ );
+
+ @Override
+ public void applyTransformation(@NotNull TransformationContext context) {
+ GrTypeDefinition clazz = context.getCodeClass();
+ if (GrailsUtils.calculateArtifactType(clazz) != GrailsArtifact.DOMAIN) return;
+
+ GormVersion gormVersion = GormVersion.forElement(clazz);
+ if (gormVersion == null) return;
+ if (gormVersion.compareTo(GormVersion.IS_5) < 0) return;
+ if (gormVersion.compareTo(GormVersion.IS_6) >= 0 && context.isInheritor("grails.gorm.rx.RxEntity")) return;
+
+ /*
+ Repeat org.grails.compiler.gorm.GormEntityTransformation#pickGormEntityTrait().
+ */
+ String mapWith = computeMapWithValue(clazz);
+ boolean hibernatePresent = JavaPsiFacade.getInstance(clazz.getProject())
+ .findClass("org.hibernate.Hibernate", clazz.getResolveScope()) != null;
+ String traitFqn;
+ if (hibernatePresent && mapWith == null) {
+ traitFqn = GormClassNames.ENTITY_TRAIT;
+ }
+ else {
+ Collection<PsiClass> providers = findTraitsFromProviders(clazz);
+ if (providers.isEmpty()) {
+ traitFqn = GormClassNames.ENTITY_TRAIT;
+ }
+ else if (mapWith == null || mapWith.isEmpty()) {
+ traitFqn = providers.size() == 1 ? qualifiedNameOrEntityTrait(providers.iterator().next())
+ : GormClassNames.ENTITY_TRAIT;
+ }
+ else {
+ PsiClass traitFromProvider = null;
+ for (PsiClass provider : providers) {
+ String name = provider.getName();
+ if (name != null && name.startsWith(mapWith)) {
+ traitFromProvider = provider;
+ break;
+ }
+ }
+ traitFqn = traitFromProvider == null ? GormClassNames.ENTITY_TRAIT : qualifiedNameOrEntityTrait(traitFromProvider);
+ }
+ }
+ Grails3TraitInjectorContributor.injectTraits(clazz, context, List.of(traitFqn));
+ }
+
+ private static @NotNull String qualifiedNameOrEntityTrait(@NotNull PsiClass clazz) {
+ String qualifiedName = clazz.getQualifiedName();
+ return qualifiedName == null ? GormClassNames.ENTITY_TRAIT : qualifiedName;
+ }
+
+ private static @Nullable String computeMapWithValue(@NotNull GrTypeDefinition clazz) {
+ return CachedValuesManager.getCachedValue(
+ clazz,
+ () -> Result.create(doComputeMapWithValue(clazz), PsiModificationTracker.MODIFICATION_COUNT));
+ }
+
+ private static @Nullable String doComputeMapWithValue(@NotNull GrTypeDefinition clazz) {
+ PsiField mapWith = clazz.findCodeFieldByName("mapWith", true);
+ if (mapWith instanceof GrField grField) {
+ GrExpression initializer = grField.getInitializerGroovy();
+ if (initializer instanceof GrLiteral literal && literal.getValue() instanceof String value) {
+ if (value.isEmpty()) return value;
+ return Character.toUpperCase(value.charAt(0)) + value.substring(1);
+ }
+ }
+ return null;
+ }
+
+ private static @NotNull Collection<PsiClass> findTraitsFromProviders(@NotNull PsiElement context) {
+ return CachedValuesManager.getCachedValue(
+ context,
+ () -> Result.create(doFindTraitsFromProviders(context), ProjectRootManager.getInstance(context.getProject())));
+ }
+
+ /**
+ * Grails loads GormEntityTraitProvider using Java Services API and calls getEntityTrait().
+ * To reduce processing time we assume that GormEntity's inheritors are registered in META-INF/services via GormEntityTraitProvider.
+ * I.e. we do not run script with user classpath to determine traits, but just search for GormEntity inheritors.
+ */
+ private static @NotNull Collection<PsiClass> doFindTraitsFromProviders(@NotNull PsiElement context) {
+ GlobalSearchScope scope = context.getResolveScope();
+ JavaPsiFacade facade = JavaPsiFacade.getInstance(context.getProject());
+ PsiClass gormEntity = facade.findClass(GormClassNames.ENTITY_TRAIT, scope);
+ if (gormEntity == null) return Collections.emptyList();
+ GormVersion version = GormVersion.forElement(context);
+ if (version == null) return Collections.emptyList();
+
+ List<PsiClass> traits = new ArrayList<>();
+ gormEntity.putUserData(GroovyDirectInheritorsSearcher.IGNORE_INHERITANCE_CHECK, true);
+ try {
+ for (PsiClass candidate : ClassInheritorsSearch.search(gormEntity, scope, true).findAll()) {
+ if (GrTraitUtil.isTrait(candidate)) traits.add(candidate);
+ }
+ }
+ finally {
+ gormEntity.putUserData(GroovyDirectInheritorsSearcher.IGNORE_INHERITANCE_CHECK, null);
+ }
+
+ // Dedupe by qualified name: the same trait can be seen through several scopes.
+ Collection<PsiClass> result = new ObjectOpenCustomHashSet<>(new Hash.Strategy<>() {
+ @Override
+ public int hashCode(@Nullable PsiClass o) {
+ String qualifiedName = o == null ? null : o.getQualifiedName();
+ return qualifiedName == null ? 0 : qualifiedName.hashCode();
+ }
+
+ @Override
+ public boolean equals(@Nullable PsiClass o1, @Nullable PsiClass o2) {
+ if (o1 == o2) return true;
+ String q1 = o1 == null ? null : o1.getQualifiedName();
+ String q2 = o2 == null ? null : o2.getQualifiedName();
+ return Objects.equals(q1, q2);
+ }
+ });
+
+ for (PsiClass trait : traits) {
+ // From GORM 6 a DB-specific trait only applies when its driver is on the classpath.
+ if (version.compareTo(GormVersion.IS_6) >= 0) {
+ String markerFqn = MARKER_CLASSES.get(trait.getQualifiedName());
+ if (markerFqn != null && facade.findClass(markerFqn, scope) == null) continue;
+ }
+ result.add(trait);
+ }
+
+ return result;
+ }
+}
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/lang/GormTraitContributor.kt b/plugin/src/main/java/org/jetbrains/plugins/grails/lang/GormTraitContributor.kt
deleted file mode 100644
index 3d024cb..0000000
--- a/plugin/src/main/java/org/jetbrains/plugins/grails/lang/GormTraitContributor.kt
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * Copyright 2000-2026 JetBrains s.r.o. and contributors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.jetbrains.plugins.grails.lang
-
-import com.intellij.openapi.roots.ProjectRootManager
-import com.intellij.openapi.util.removeUserData
-import com.intellij.psi.JavaPsiFacade
-import com.intellij.psi.PsiClass
-import com.intellij.psi.PsiElement
-import com.intellij.psi.search.searches.ClassInheritorsSearch
-import com.intellij.psi.util.CachedValueProvider.Result
-import com.intellij.psi.util.CachedValuesManager
-import com.intellij.psi.util.PsiModificationTracker
-import it.unimi.dsi.fastutil.Hash
-import it.unimi.dsi.fastutil.objects.ObjectOpenCustomHashSet
-import org.jetbrains.plugins.grails.gorm.GormClassNames.ENTITY_TRAIT
-import org.jetbrains.plugins.grails.gorm.GormVersion
-import org.jetbrains.plugins.grails.gorm.getGormVersion
-import org.jetbrains.plugins.grails.util.GrailsArtifact
-import org.jetbrains.plugins.grails.util.GrailsUtils
-import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrField
-import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrLiteral
-import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrTypeDefinition
-import org.jetbrains.plugins.groovy.lang.psi.impl.GroovyDirectInheritorsSearcher
-import org.jetbrains.plugins.groovy.lang.psi.util.GrTraitUtil
-import org.jetbrains.plugins.groovy.transformations.AstTransformationSupport
-import org.jetbrains.plugins.groovy.transformations.TransformationContext
-
-class GormTraitContributor : AstTransformationSupport {
-
- override fun applyTransformation(context: TransformationContext) {
- val clazz = context.codeClass
- if (GrailsUtils.calculateArtifactType(clazz) !== GrailsArtifact.DOMAIN) return
-
- val gormVersion = getGormVersion(clazz) ?: return
- if (gormVersion < GormVersion.IS_5) return
- if (gormVersion >= GormVersion.IS_6 && context.isInheritor("grails.gorm.rx.RxEntity")) return
-
- /*
- Repeat org.grails.compiler.gorm.GormEntityTransformation#pickGormEntityTrait().
- */
- val mapWith = computeMapWithValue(clazz)
- val hibernatePresent = JavaPsiFacade.getInstance(clazz.project).findClass("org.hibernate.Hibernate", clazz.resolveScope) != null
- val traitFqn = if (hibernatePresent && mapWith == null) {
- ENTITY_TRAIT
- }
- else {
- val providers = findTraitsFromProviders(clazz)
- if (providers.isEmpty()) {
- ENTITY_TRAIT
- }
- else if (mapWith == null || mapWith.isEmpty()) {
- providers.singleOrNull()?.qualifiedName ?: ENTITY_TRAIT
- }
- else {
- val traitFromProvider = providers.find { it.name?.startsWith(mapWith) ?: false }
- traitFromProvider?.qualifiedName ?: ENTITY_TRAIT
- }
- }
- Grails3TraitInjectorContributor.injectTraits(clazz, context, listOf(traitFqn))
- }
-
- private fun computeMapWithValue(clazz: GrTypeDefinition) = CachedValuesManager.getCachedValue(clazz) {
- Result.create(doComputeMapWithValue(clazz), PsiModificationTracker.MODIFICATION_COUNT)
- }
-
- private fun doComputeMapWithValue(clazz: GrTypeDefinition): String? {
- val mapWith = clazz.findCodeFieldByName("mapWith", true)
- if (mapWith is GrField) {
- val initializer = mapWith.initializerGroovy
- if (initializer is GrLiteral) {
- val value = initializer.value
- if (value is String) {
- return value.replaceFirstChar { it.uppercaseChar() }
- }
- }
- }
- return null
- }
-
- private fun findTraitsFromProviders(context: PsiElement) = CachedValuesManager.getCachedValue(context) {
- Result.create(doFindTraitsFromProviders(context), ProjectRootManager.getInstance(context.project))
- }
-
- /**
- * Grails loads GormEntityTraitProvider using Java Services API and calls getEntityTrait().
- * To reduce processing time we assume that GormEntity's inheritors are registered in META-INF/services via GormEntityTraitProvider.
- * I.e. we do not run script with user classpath to determine traits, but just search for GormEntity inheritors.
- */
- private fun doFindTraitsFromProviders(context: PsiElement): Collection<PsiClass> {
- val scope = context.resolveScope
- val facade = JavaPsiFacade.getInstance(context.project)
- val gormEntity = facade.findClass(ENTITY_TRAIT, scope) ?: return emptyList()
- val version = getGormVersion(context) ?: return emptyList()
- val result = ObjectOpenCustomHashSet(object : Hash.Strategy<PsiClass> {
- override fun hashCode(`object`: PsiClass?) = `object`?.qualifiedName?.hashCode() ?: 0
- override fun equals(o1: PsiClass?, o2: PsiClass?) = o1 === o2 || o1?.qualifiedName == o2?.qualifiedName
- })
- gormEntity.putUserData(GroovyDirectInheritorsSearcher.IGNORE_INHERITANCE_CHECK, true)
- val query = ClassInheritorsSearch.search(gormEntity, scope, true)
- val traits = query.findAll().asSequence().filter {
- GrTraitUtil.isTrait(it)
- }
- gormEntity.removeUserData(GroovyDirectInheritorsSearcher.IGNORE_INHERITANCE_CHECK)
- val availableTraits = if (version < GormVersion.IS_6) traits
- else traits.filter {
- val markerFqn = markerClasses[it.qualifiedName] ?: return@filter true
- facade.findClass(markerFqn, scope) != null
- }
-
- return availableTraits.toCollection(result)
- }
-
- private val markerClasses = mapOf(
- "grails.mongodb.MongoEntity" to "com.mongodb.MongoClient",
- "grails.neo4j.Neo4jEntity" to "org.neo4j.driver.v1.Driver"
- )
-}
\ No newline at end of file
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/references/domain/GormAstTransformationContributor.java b/plugin/src/main/java/org/jetbrains/plugins/grails/references/domain/GormAstTransformationContributor.java
index e058189..f12b900 100644
--- a/plugin/src/main/java/org/jetbrains/plugins/grails/references/domain/GormAstTransformationContributor.java
+++ b/plugin/src/main/java/org/jetbrains/plugins/grails/references/domain/GormAstTransformationContributor.java
@@ -29,7 +29,6 @@ import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.plugins.grails.gorm.GormVersion;
-import org.jetbrains.plugins.grails.gorm.GormVersionKt;
import org.jetbrains.plugins.grails.util.GrailsArtifactTransformerUtils;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrField;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrTypeDefinition;
@@ -74,7 +73,7 @@ public final class GormAstTransformationContributor implements AstTransformation
final PsiType myMapType = TypesUtil.createTypeByFQClassName(CommonClassNames.JAVA_UTIL_MAP, aClass);
context.addMethod(new GrLightMethodBuilder(aClass).addParameter("namedArgs", myMapType));
- GormVersion version = GormVersionKt.getGormVersion(aClass);
+ GormVersion version = GormVersion.forElement(aClass);
if (version != GormVersion.BELOW_4) return;
List<PsiMethod> methods = new ArrayList<>();
diff --git a/pluginModules/maven/build.gradle b/pluginModules/maven/build.gradle
index 7ab37e9..9235ca5 100644
--- a/pluginModules/maven/build.gradle
+++ b/pluginModules/maven/build.gradle
@@ -15,7 +15,6 @@
*/
plugins {
id 'org.apache.grails.intellij.build.intellij-module'
- id 'org.apache.grails.intellij.build.kotlin'
}
dependencies {
diff --git a/pluginModules/maven/src/main/java/com/intellij/groovy/grails/maven/GrailsMavenWorkspaceConfigurator.java b/pluginModules/maven/src/main/java/com/intellij/groovy/grails/maven/GrailsMavenWorkspaceConfigurator.java
new file mode 100644
index 0000000..de0e653
--- /dev/null
+++ b/pluginModules/maven/src/main/java/com/intellij/groovy/grails/maven/GrailsMavenWorkspaceConfigurator.java
@@ -0,0 +1,103 @@
+/*
+ * 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.
+ */
+package com.intellij.groovy.grails.maven;
+
+import com.intellij.facet.FacetManager;
+import com.intellij.facet.ModifiableFacetModel;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.module.Module;
+import com.intellij.util.Consumer;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.idea.maven.importing.MavenStaticSyncAware;
+import org.jetbrains.idea.maven.importing.MavenWorkspaceConfigurator;
+import org.jetbrains.idea.maven.project.MavenProject;
+import org.jetbrains.jps.model.module.JpsModuleSourceRootType;
+import org.jetbrains.plugins.grails.config.GrailsFramework;
+import org.jetbrains.plugins.grails.util.GrailsFacetProvider;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Stream;
+
+/**
+ * Replaces the legacy {@link org.jetbrains.idea.maven.importing.MavenImporter}-based
+ * GrailsMavenImporter, which is no longer invoked by the workspace-model Maven import
+ * (2026.2): registers the Grails source/test/resource roots and the Grails facets for
+ * projects built with org.grails:grails-maven-plugin.
+ */
+public final class GrailsMavenWorkspaceConfigurator implements MavenWorkspaceConfigurator, MavenStaticSyncAware {
+
+ private static boolean isApplicable(@NotNull MavenProject mavenProject) {
+ return mavenProject.findPlugin("org.grails", "grails-maven-plugin") != null;
+ }
+
+ @Override
+ public @NotNull Stream<AdditionalFolder> getAdditionalFolders(@NotNull FoldersContext context) {
+ if (!isApplicable(context.getMavenProject())) {
+ return Stream.empty();
+ }
+ List<AdditionalFolder> folders = new ArrayList<>();
+ for (JpsModuleSourceRootType<?> rootType : GrailsFramework.GRAILS_SOURCE_FOLDERS.keySet()) {
+ FolderType folderType = toFolderType(rootType);
+ for (String path : GrailsFramework.GRAILS_SOURCE_FOLDERS.get(rootType)) {
+ folders.add(new AdditionalFolder(path, folderType));
+ }
+ }
+ // the static preimport repairs default source dirs but not resources
+ // (MavenProjectStaticImporter.resolveDirectories), so restore the Maven defaults here
+ if (context.getMavenProject().getResources().isEmpty()) {
+ folders.add(new AdditionalFolder("src/main/resources", FolderType.RESOURCE));
+ }
+ if (context.getMavenProject().getTestResources().isEmpty()) {
+ folders.add(new AdditionalFolder("src/test/resources", FolderType.TEST_RESOURCE));
+ }
+ return folders.stream();
+ }
+
+ @Override
+ public void afterModelApplied(@NotNull AppliedModelContext context) {
+ // getMavenProjectsWithModules() is a kotlin.sequences.Sequence, which is not Iterable, so it
+ // cannot drive a for-each from Java.
+ var projects = context.getMavenProjectsWithModules().iterator();
+ while (projects.hasNext()) {
+ var projectWithModules = projects.next();
+ MavenProject mavenProject = projectWithModules.getMavenProject();
+ if (!isApplicable(mavenProject)) continue;
+
+ for (var moduleWithType : projectWithModules.getModules()) {
+ Module module = moduleWithType.getModule();
+ List<Consumer<ModifiableFacetModel>> actions = new ArrayList<>();
+ for (GrailsFacetProvider provider : GrailsFacetProvider.EP_NAME.getExtensions()) {
+ provider.addFacets(actions, module, List.of(mavenProject.getDirectoryFile()));
+ }
+ if (actions.isEmpty()) continue;
+
+ ApplicationManager.getApplication().invokeAndWait(
+ () -> ApplicationManager.getApplication().runWriteAction(() -> {
+ if (module.isDisposed()) return;
+ ModifiableFacetModel facetModel = FacetManager.getInstance(module).createModifiableModel();
+ for (Consumer<ModifiableFacetModel> action : actions) {
+ action.consume(facetModel);
+ }
+ facetModel.commit();
+ }));
+ }
+ }
+ }
+}
diff --git a/pluginModules/maven/src/main/java/com/intellij/groovy/grails/maven/GrailsMavenWorkspaceConfigurator.kt b/pluginModules/maven/src/main/java/com/intellij/groovy/grails/maven/GrailsMavenWorkspaceConfigurator.kt
deleted file mode 100644
index 3258e86..0000000
--- a/pluginModules/maven/src/main/java/com/intellij/groovy/grails/maven/GrailsMavenWorkspaceConfigurator.kt
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * 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.
- */
-package com.intellij.groovy.grails.maven
-
-import com.intellij.facet.FacetManager
-import com.intellij.facet.ModifiableFacetModel
-import com.intellij.openapi.application.ApplicationManager
-import com.intellij.util.Consumer
-import org.jetbrains.idea.maven.importing.MavenStaticSyncAware
-import org.jetbrains.idea.maven.importing.MavenWorkspaceConfigurator
-import org.jetbrains.idea.maven.project.MavenProject
-import org.jetbrains.plugins.grails.config.GrailsFramework
-import org.jetbrains.plugins.grails.util.GrailsFacetProvider
-import java.util.stream.Stream
-
-/**
- * Replaces the legacy [org.jetbrains.idea.maven.importing.MavenImporter]-based
- * GrailsMavenImporter, which is no longer invoked by the workspace-model Maven import
- * (2026.2): registers the Grails source/test/resource roots and the Grails facets for
- * projects built with org.grails:grails-maven-plugin.
- */
-internal class GrailsMavenWorkspaceConfigurator : MavenWorkspaceConfigurator, MavenStaticSyncAware {
-
- private fun isApplicable(mavenProject: MavenProject): Boolean =
- mavenProject.findPlugin("org.grails", "grails-maven-plugin") != null
-
- override fun getAdditionalFolders(
- context: MavenWorkspaceConfigurator.FoldersContext,
- ): Stream<MavenWorkspaceConfigurator.AdditionalFolder> {
- if (!isApplicable(context.mavenProject)) {
- return Stream.empty()
- }
- val folders = ArrayList<MavenWorkspaceConfigurator.AdditionalFolder>()
- for (rootType in GrailsFramework.GRAILS_SOURCE_FOLDERS.keySet()) {
- val folderType = rootType.toFolderType()
- GrailsFramework.GRAILS_SOURCE_FOLDERS.get(rootType)
- .mapTo(folders) { MavenWorkspaceConfigurator.AdditionalFolder(it, folderType) }
- }
- // the static preimport repairs default source dirs but not resources
- // (MavenProjectStaticImporter.resolveDirectories), so restore the Maven defaults here
- if (context.mavenProject.resources.isEmpty()) {
- folders.add(MavenWorkspaceConfigurator.AdditionalFolder("src/main/resources", MavenWorkspaceConfigurator.FolderType.RESOURCE))
- }
- if (context.mavenProject.testResources.isEmpty()) {
- folders.add(MavenWorkspaceConfigurator.AdditionalFolder("src/test/resources", MavenWorkspaceConfigurator.FolderType.TEST_RESOURCE))
- }
- return folders.stream()
- }
-
- override fun afterModelApplied(context: MavenWorkspaceConfigurator.AppliedModelContext) {
- for (projectWithModules in context.mavenProjectsWithModules) {
- val mavenProject = projectWithModules.mavenProject
- if (!isApplicable(mavenProject)) continue
-
- for (moduleWithType in projectWithModules.modules) {
- val module = moduleWithType.module
- val actions = ArrayList<Consumer<ModifiableFacetModel>>()
- for (provider in GrailsFacetProvider.EP_NAME.extensions) {
- provider.addFacets(actions, module, listOf(mavenProject.directoryFile))
- }
- if (actions.isEmpty()) continue
-
- ApplicationManager.getApplication().invokeAndWait {
- ApplicationManager.getApplication().runWriteAction {
- if (module.isDisposed) return@runWriteAction
- val facetModel = FacetManager.getInstance(module).createModifiableModel()
- actions.forEach { it.consume(facetModel) }
- facetModel.commit()
- }
- }
- }
- }
- }
-}
diff --git a/pluginModules/maven/src/main/java/com/intellij/groovy/grails/maven/MavenNodeProvider.java b/pluginModules/maven/src/main/java/com/intellij/groovy/grails/maven/MavenNodeProvider.java
new file mode 100644
index 0000000..49641da
--- /dev/null
+++ b/pluginModules/maven/src/main/java/com/intellij/groovy/grails/maven/MavenNodeProvider.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2000-2026 JetBrains s.r.o. and contributors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.groovy.grails.maven;
+
+import com.intellij.ide.projectView.ViewSettings;
+import com.intellij.ide.projectView.impl.nodes.PsiFileNode;
+import com.intellij.ide.util.treeView.AbstractTreeNode;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.idea.maven.model.MavenConstants;
+import org.jetbrains.plugins.grails.projectView.api.GrailsSingleNodeProvider;
+import org.jetbrains.plugins.grails.structure.GrailsApplication;
+
+/** Shows the {@code pom.xml} of a Maven-built Grails application in the Grails project view. */
+public final class MavenNodeProvider extends GrailsSingleNodeProvider {
+
+ @Override
+ public @Nullable AbstractTreeNode<?> createNode(@NotNull GrailsApplication application, @NotNull ViewSettings settings) {
+ if (!(application instanceof GrailsMavenApplication)) return null;
+ Project project = application.getProject();
+ VirtualFile pom = application.getRoot().findChild(MavenConstants.POM_XML);
+ if (pom == null) return null;
+ PsiFile psiFile = PsiManager.getInstance(project).findFile(pom);
+ if (psiFile == null) return null;
+ return new PsiFileNode(project, psiFile, settings);
+ }
+}
diff --git a/pluginModules/maven/src/main/java/com/intellij/groovy/grails/maven/MavenNodeProvider.kt b/pluginModules/maven/src/main/java/com/intellij/groovy/grails/maven/MavenNodeProvider.kt
deleted file mode 100644
index 21798d9..0000000
--- a/pluginModules/maven/src/main/java/com/intellij/groovy/grails/maven/MavenNodeProvider.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright 2000-2026 JetBrains s.r.o. and contributors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.intellij.groovy.grails.maven
-
-import com.intellij.ide.projectView.ViewSettings
-import com.intellij.ide.projectView.impl.nodes.PsiFileNode
-import com.intellij.ide.util.treeView.AbstractTreeNode
-import com.intellij.psi.PsiManager
-import org.jetbrains.idea.maven.model.MavenConstants
-import org.jetbrains.plugins.grails.projectView.api.GrailsSingleNodeProvider
-import org.jetbrains.plugins.grails.structure.GrailsApplication
-
-internal class MavenNodeProvider : GrailsSingleNodeProvider() {
-
- override fun createNode(application: GrailsApplication, settings: ViewSettings): AbstractTreeNode<*>? {
- if (application !is GrailsMavenApplication) return null
- val project = application.project
- return application.root.findChild(MavenConstants.POM_XML)
- ?.let { PsiManager.getInstance(project).findFile(it) }
- ?.let { PsiFileNode(project, it, settings) }
- }
-}
\ No newline at end of file