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 -&gt; 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