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:44 UTC
(grails-intellij-plugin) 24/35: Convert the structure package from 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 83356c517c644379c998352d3bd28f69f3d01dc2
Author: James Daugherty <jd...@jdresources.net>
AuthorDate: Sun Jul 26 13:55:54 2026 -0400
Convert the structure package from Kotlin to Java
14 Kotlin files become 15 Java files, leaving no Kotlin under structure/.
987 tests / 0 failures, RAT clean.
This package had to be converted as one unit rather than file by file, which is a
constraint the earlier batches did not hit: Kotlin's `override val project` cannot
override a Java getter. Converting GrailsApplication alone produced
"'project' overrides nothing" from every Kotlin subclass, because Kotlin
synthesises property syntax for *reading* a Java getter but requires
`override fun getProject()` to implement one. So a class hierarchy crossing the
language boundary is not partially convertible -- interface and implementors move
together.
Two smaller translation traps, both caught by the compiler:
* Grails2Application was Kotlin `internal`, which is public in bytecode, so the
runner package could reach it across packages. Package-private in Java could not;
it is public with @ApiStatus.Internal, the same fix the projectView batch needed
for its plugin.xml-instantiated classes.
* GrailsSettings.properties is a public FIELD, not a getter. Kotlin's
`settings.properties[key]` reads fields and getters identically, so the literal
translation to .getProperties() does not compile.
Naming: structure/util.kt held a single @JvmField COMPARATOR, so Java already
referenced it as UtilKt.COMPARATOR; it becomes GrailsApplications.COMPARATOR, still
a static field, with the one import updated in GrailsApplicationCombobox.
impl/util.kt's nullable-receiver extension becomes GrailsProperties.getPropertyValue.
`by lazy` / `lazyUnsafe` fields become volatile lazily-computed fields, as in the
earlier batches. Each is derived from immutable external-system data or the VFS, so
an idempotent recompute under a race is harmless and only publication needs care.
62 files left in the conversion target set.
---
.../runner/ui/GrailsApplicationCombobox.java | 2 +-
...ails3Application.kt => Grails3Application.java} | 10 +-
.../grails/structure/GrailsApplication.java | 58 +++++++++
.../plugins/grails/structure/GrailsApplication.kt | 54 --------
.../grails/structure/GrailsApplicationBase.java | 86 +++++++++++++
.../grails/structure/GrailsApplicationBase.kt | 58 ---------
.../grails/structure/GrailsApplications.java | 47 +++++++
.../grails/structure/GrailsCommonClassNames.java | 143 +++++++++++++++++++++
.../grails/structure/GrailsCommonClassNames.kt | 84 ------------
...ilsApplication.kt => OldGrailsApplication.java} | 15 ++-
.../grails/structure/impl/Grails2Application.java | 73 +++++++++++
.../grails/structure/impl/Grails2Application.kt | 46 -------
.../structure/impl/Grails3ApplicationBase.java | 91 +++++++++++++
.../structure/impl/Grails3ApplicationBase.kt | 55 --------
.../structure/impl/Grails3ApplicationProvider.java | 64 +++++++++
.../structure/impl/Grails3ApplicationProvider.kt | 54 --------
.../impl/Grails3MultiModuleApplication.java | 93 ++++++++++++++
.../impl/Grails3MultiModuleApplication.kt | 55 --------
.../impl/Grails3SingleModuleApplication.java | 57 ++++++++
.../impl/Grails3SingleModuleApplication.kt | 51 --------
.../impl/GrailsModuleBasedApplication.java | 77 +++++++++++
.../structure/impl/GrailsModuleBasedApplication.kt | 60 ---------
.../grails/structure/impl/GrailsProperties.java | 34 +++++
.../impl/OldGrailsModuleBasedApplication.java | 81 ++++++++++++
.../impl/OldGrailsModuleBasedApplication.kt | 69 ----------
.../plugins/grails/structure/impl/util.kt | 21 ---
.../org/jetbrains/plugins/grails/structure/util.kt | 27 ----
27 files changed, 918 insertions(+), 647 deletions(-)
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/runner/ui/GrailsApplicationCombobox.java b/plugin/src/main/java/org/jetbrains/plugins/grails/runner/ui/GrailsApplicationCombobox.java
index 6b81b2b..e210a5a 100644
--- a/plugin/src/main/java/org/jetbrains/plugins/grails/runner/ui/GrailsApplicationCombobox.java
+++ b/plugin/src/main/java/org/jetbrains/plugins/grails/runner/ui/GrailsApplicationCombobox.java
@@ -29,7 +29,7 @@ import org.jetbrains.plugins.grails.structure.GrailsApplication;
import java.util.Collection;
-import static org.jetbrains.plugins.grails.structure.UtilKt.COMPARATOR;
+import static org.jetbrains.plugins.grails.structure.GrailsApplications.COMPARATOR;
public class GrailsApplicationCombobox extends ComboBox<GrailsApplication> {
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/Grails3Application.kt b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/Grails3Application.java
similarity index 70%
rename from plugin/src/main/java/org/jetbrains/plugins/grails/structure/Grails3Application.kt
rename to plugin/src/main/java/org/jetbrains/plugins/grails/structure/Grails3Application.java
index efc17f4..1f24d56 100644
--- a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/Grails3Application.kt
+++ b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/Grails3Application.java
@@ -13,12 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package org.jetbrains.plugins.grails.structure;
-package org.jetbrains.plugins.grails.structure
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.plugins.grails.gradle.GrailsModuleData;
-import org.jetbrains.plugins.grails.gradle.GrailsModuleData
+public interface Grails3Application extends GrailsApplication {
-interface Grails3Application : GrailsApplication {
-
- val gradleData: GrailsModuleData
+ @NotNull GrailsModuleData getGradleData();
}
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/GrailsApplication.java b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/GrailsApplication.java
new file mode 100644
index 0000000..2b3af1c
--- /dev/null
+++ b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/GrailsApplication.java
@@ -0,0 +1,58 @@
+/*
+ * 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.structure;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.NlsSafe;
+import com.intellij.openapi.util.UserDataHolderEx;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.search.GlobalSearchScope;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.plugins.grails.util.version.Version;
+
+import javax.swing.Icon;
+
+public interface GrailsApplication extends UserDataHolderEx {
+
+ @NlsSafe @NotNull String getName();
+
+ default @Nullable String getAppVersion() {
+ return null;
+ }
+
+ @NotNull Icon getIcon();
+
+ @NotNull Project getProject();
+
+ /**
+ * @return folder that contains grails-app, application.properties, etc.
+ */
+ @NotNull VirtualFile getRoot();
+
+ /**
+ * @return {@code grails-app} folder
+ */
+ @NotNull VirtualFile getAppRoot();
+
+ @NotNull Version getGrailsVersion();
+
+ boolean isValid();
+
+ void invalidate();
+
+ @NotNull GlobalSearchScope getScope(boolean includeDependencies, boolean testsOnly);
+}
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/GrailsApplication.kt b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/GrailsApplication.kt
deleted file mode 100644
index 82308d4..0000000
--- a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/GrailsApplication.kt
+++ /dev/null
@@ -1,54 +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.structure
-
-import com.intellij.openapi.project.Project
-import com.intellij.openapi.util.NlsSafe
-import com.intellij.openapi.util.UserDataHolderEx
-import com.intellij.openapi.vfs.VirtualFile
-import com.intellij.psi.search.GlobalSearchScope
-import org.jetbrains.plugins.grails.util.version.Version
-import javax.swing.Icon
-
-interface GrailsApplication : UserDataHolderEx {
-
- val name: @NlsSafe String
-
- val appVersion: String? get() = null
-
- val icon: Icon
-
- val project: Project
-
- /**
- * @return folder that contains grails-app, application.properties, etc.
- */
- val root: VirtualFile
-
- /**
- * @return `grails-app` folder
- */
- val appRoot: VirtualFile
-
- val grailsVersion: Version
-
- val isValid: Boolean
-
- fun invalidate()
-
- fun getScope(includeDependencies: Boolean, testsOnly: Boolean): GlobalSearchScope
-}
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/GrailsApplicationBase.java b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/GrailsApplicationBase.java
new file mode 100644
index 0000000..c6d3456
--- /dev/null
+++ b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/GrailsApplicationBase.java
@@ -0,0 +1,86 @@
+/*
+ * 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.structure;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.UserDataHolderBase;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.plugins.grails.GroovyMvcIcons;
+import org.jetbrains.plugins.grails.config.GrailsConstants;
+
+import javax.swing.Icon;
+import java.util.Objects;
+
+public abstract class GrailsApplicationBase extends UserDataHolderBase implements GrailsApplication {
+
+ private final Project myProject;
+ private final VirtualFile myRoot;
+ private boolean myValid = true;
+
+ protected GrailsApplicationBase(@NotNull Project project, @NotNull VirtualFile root) {
+ myProject = project;
+ myRoot = root;
+ }
+
+ @Override
+ public @NotNull Project getProject() {
+ return myProject;
+ }
+
+ @Override
+ public @NotNull VirtualFile getRoot() {
+ return myRoot;
+ }
+
+ @Override
+ public @NotNull VirtualFile getAppRoot() {
+ return Objects.requireNonNull(myRoot.findChild(GrailsConstants.APP_DIRECTORY));
+ }
+
+ @Override
+ public @NotNull Icon getIcon() {
+ return GroovyMvcIcons.Grails_app;
+ }
+
+ @Override
+ public boolean isValid() {
+ return myValid && myRoot.findChild(GrailsConstants.APP_DIRECTORY) != null && !myProject.isDisposed();
+ }
+
+ @Override
+ public void invalidate() {
+ myValid = false;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) return true;
+ if (other == null || other.getClass() != getClass()) return false;
+ GrailsApplicationBase that = (GrailsApplicationBase)other;
+ return myProject.equals(that.myProject) && myRoot.equals(that.myRoot);
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * myProject.hashCode() + myRoot.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "{name: " + getName() + ", root: " + myRoot + ", version: " + getGrailsVersion() + "}";
+ }
+}
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/GrailsApplicationBase.kt b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/GrailsApplicationBase.kt
deleted file mode 100644
index 09cefe5..0000000
--- a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/GrailsApplicationBase.kt
+++ /dev/null
@@ -1,58 +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.structure
-
-import com.intellij.openapi.project.Project
-import com.intellij.openapi.util.UserDataHolderBase
-import com.intellij.openapi.vfs.VirtualFile
-import org.jetbrains.plugins.grails.GroovyMvcIcons
-import org.jetbrains.plugins.grails.config.GrailsConstants
-import javax.swing.Icon
-
-abstract class GrailsApplicationBase(
- override val project: Project,
- override val root: VirtualFile
-) : UserDataHolderBase(), GrailsApplication {
-
- override val appRoot: VirtualFile get() = root.findChild(GrailsConstants.APP_DIRECTORY)!!
-
- override val icon: Icon = GroovyMvcIcons.Grails_app
-
- private var myValid = true
-
- override val isValid: Boolean get() = myValid && root.findChild(GrailsConstants.APP_DIRECTORY) != null && !project.isDisposed
-
- override fun invalidate() {
- myValid = false
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other?.javaClass != javaClass) return false
-
- other as GrailsApplicationBase
-
- if (project != other.project) return false
- if (root != other.root) return false
-
- return true
- }
-
- override fun hashCode(): Int = 31 * project.hashCode() + root.hashCode()
-
- override fun toString(): String = "${javaClass.simpleName}{name: $name, root: $root, version: $grailsVersion}"
-}
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/GrailsApplications.java b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/GrailsApplications.java
new file mode 100644
index 0000000..344ed61
--- /dev/null
+++ b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/GrailsApplications.java
@@ -0,0 +1,47 @@
+/*
+ * 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.structure;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Comparator;
+
+public final class GrailsApplications {
+
+ /**
+ * Orders applications by name, case-insensitively, with nulls first.
+ *
+ * <p>Was a top-level {@code @JvmField val} in structure/util.kt, so Java already referenced it
+ * as {@code UtilKt.COMPARATOR}; it keeps the same shape as a static field here.
+ */
+ public static final Comparator<GrailsApplication> COMPARATOR = new Comparator<>() {
+ @Override
+ public int compare(@Nullable GrailsApplication app1, @Nullable GrailsApplication app2) {
+ if (app1 == null && app2 == null) return 0;
+ if (app1 == null) return -1;
+ if (app2 == null) return 1;
+ return app1.getName().compareToIgnoreCase(app2.getName());
+ }
+ };
+
+ private GrailsApplications() {
+ }
+
+ static @NotNull Comparator<GrailsApplication> comparator() {
+ return COMPARATOR;
+ }
+}
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/GrailsCommonClassNames.java b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/GrailsCommonClassNames.java
new file mode 100644
index 0000000..ee384b1
--- /dev/null
+++ b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/GrailsCommonClassNames.java
@@ -0,0 +1,143 @@
+/*
+ * 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.structure;
+
+import com.intellij.psi.PsiElement;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.plugins.grails.util.version.Version;
+
+/**
+ * Fully qualified names of Grails/GORM classes whose packages moved between major versions.
+ * The prefix and the constraint sub-package differ per era, so callers resolve an instance for
+ * the context's application rather than hardcoding names.
+ */
+public final class GrailsCommonClassNames {
+
+ private static final String HIBERNATE_MAPPING_BUILDER = "orm.hibernate.cfg.HibernateMappingBuilder";
+ private static final String CREDIT_CARD_CONSTRAINT = "CreditCardConstraint";
+ private static final String MAX_SIZE_CONSTRAINT = "MaxSizeConstraint";
+ private static final String EMAIL_CONSTRAINT = "EmailConstraint";
+ private static final String BLANK_CONSTRAINT = "BlankConstraint";
+ private static final String RANGE_CONSTRAINT = "RangeConstraint";
+ private static final String URL_CONSTRAINT = "UrlConstraint";
+ private static final String SIZE_CONSTRAINT = "SizeConstraint";
+ private static final String IN_LIST_CONSTRAINT = "InListConstraint";
+ private static final String MATCHES_CONSTRAINT = "MatchesConstraint";
+ private static final String MIN_CONSTRAINT = "MinConstraint";
+ private static final String MAX_CONSTRAINT = "MaxConstraint";
+ private static final String MIN_SIZE_CONSTRAINT = "MinSizeConstraint";
+ private static final String SCALE_CONSTRAINT = "ScaleConstraint";
+ private static final String NOT_EQUAL_CONSTRAINT = "NotEqualConstraint";
+ private static final String NULLABLE_CONSTRAINT = "NullableConstraint";
+ private static final String VALIDATOR_CONSTRAINT = "ValidatorConstraint";
+ private static final String UNIQUE_CONSTRAINT = "UniqueConstraint";
+ private static final String URL_MAPPING_BUILDER = "web.mapping.DefaultUrlMappingEvaluator.UrlMappingBuilder";
+
+ private static final GrailsCommonClassNames AFTER_40 =
+ new GrailsCommonClassNames("org.grails.", "datastore.gorm.validation.constraints.");
+ private static final GrailsCommonClassNames AFTER_30 =
+ new GrailsCommonClassNames("org.grails.", "validation.");
+ private static final GrailsCommonClassNames BEFORE_30 =
+ new GrailsCommonClassNames("org.codehaus.groovy.grails.", "validation.");
+
+ private final String hibernateMappingBuilder;
+ private final String creditCardConstraint;
+ private final String emailConstraint;
+ private final String blankConstraint;
+ private final String rangeConstraint;
+ private final String inListConstraint;
+ private final String urlConstraint;
+ private final String sizeConstraint;
+ private final String matchesConstraint;
+ private final String minConstraint;
+ private final String maxConstraint;
+ private final String minSizeConstraint;
+ private final String maxSizeConstraint;
+ private final String scaleConstraint;
+ private final String notEqualConstraint;
+ private final String nullableConstraint;
+ private final String validatorConstraint;
+ private final String uniqueConstraint;
+ private final String urlMappingBuilder;
+
+ private GrailsCommonClassNames(@NotNull String globalPrefix, @NotNull String constraintPath) {
+ hibernateMappingBuilder = globalPrefix + HIBERNATE_MAPPING_BUILDER;
+ creditCardConstraint = globalPrefix + constraintPath + CREDIT_CARD_CONSTRAINT;
+ emailConstraint = globalPrefix + constraintPath + EMAIL_CONSTRAINT;
+ blankConstraint = globalPrefix + constraintPath + BLANK_CONSTRAINT;
+ rangeConstraint = globalPrefix + constraintPath + RANGE_CONSTRAINT;
+ inListConstraint = globalPrefix + constraintPath + IN_LIST_CONSTRAINT;
+ urlConstraint = globalPrefix + constraintPath + URL_CONSTRAINT;
+ sizeConstraint = globalPrefix + constraintPath + SIZE_CONSTRAINT;
+ matchesConstraint = globalPrefix + constraintPath + MATCHES_CONSTRAINT;
+ minConstraint = globalPrefix + constraintPath + MIN_CONSTRAINT;
+ maxConstraint = globalPrefix + constraintPath + MAX_CONSTRAINT;
+ minSizeConstraint = globalPrefix + constraintPath + MIN_SIZE_CONSTRAINT;
+ maxSizeConstraint = globalPrefix + constraintPath + MAX_SIZE_CONSTRAINT;
+ scaleConstraint = globalPrefix + constraintPath + SCALE_CONSTRAINT;
+ notEqualConstraint = globalPrefix + constraintPath + NOT_EQUAL_CONSTRAINT;
+ nullableConstraint = globalPrefix + constraintPath + NULLABLE_CONSTRAINT;
+ validatorConstraint = globalPrefix + constraintPath + VALIDATOR_CONSTRAINT;
+ uniqueConstraint = globalPrefix + constraintPath + UNIQUE_CONSTRAINT;
+ urlMappingBuilder = globalPrefix + URL_MAPPING_BUILDER;
+ }
+
+ public static @NotNull GrailsCommonClassNames getInstance(@NotNull PsiElement context) {
+ GrailsApplication app = GrailsApplicationManager.findApplication(context);
+ if (app == null) return BEFORE_30;
+ if (Version.AT_LEAST_4.contains(app.getGrailsVersion())) return AFTER_40;
+ if (Version.AT_LEAST_3.contains(app.getGrailsVersion())) return AFTER_30;
+ return BEFORE_30;
+ }
+
+ public @NotNull String getHibernateMappingBuilder() { return hibernateMappingBuilder; }
+
+ public @NotNull String getCreditCardConstraint() { return creditCardConstraint; }
+
+ public @NotNull String getEmailConstraint() { return emailConstraint; }
+
+ public @NotNull String getBlankConstraint() { return blankConstraint; }
+
+ public @NotNull String getRangeConstraint() { return rangeConstraint; }
+
+ public @NotNull String getInListConstraint() { return inListConstraint; }
+
+ public @NotNull String getUrlConstraint() { return urlConstraint; }
+
+ public @NotNull String getSizeConstraint() { return sizeConstraint; }
+
+ public @NotNull String getMatchesConstraint() { return matchesConstraint; }
+
+ public @NotNull String getMinConstraint() { return minConstraint; }
+
+ public @NotNull String getMaxConstraint() { return maxConstraint; }
+
+ public @NotNull String getMinSizeConstraint() { return minSizeConstraint; }
+
+ public @NotNull String getMaxSizeConstraint() { return maxSizeConstraint; }
+
+ public @NotNull String getScaleConstraint() { return scaleConstraint; }
+
+ public @NotNull String getNotEqualConstraint() { return notEqualConstraint; }
+
+ public @NotNull String getNullableConstraint() { return nullableConstraint; }
+
+ public @NotNull String getValidatorConstraint() { return validatorConstraint; }
+
+ public @NotNull String getUniqueConstraint() { return uniqueConstraint; }
+
+ public @NotNull String getUrlMappingBuilder() { return urlMappingBuilder; }
+}
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/GrailsCommonClassNames.kt b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/GrailsCommonClassNames.kt
deleted file mode 100644
index 9bcfda7..0000000
--- a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/GrailsCommonClassNames.kt
+++ /dev/null
@@ -1,84 +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.structure
-
-import com.intellij.psi.PsiElement
-import org.jetbrains.plugins.grails.util.version.Version
-
-class GrailsCommonClassNames private constructor(globalPrefix: String, constraintPath: String) {
-
- companion object {
- private val after40: GrailsCommonClassNames = GrailsCommonClassNames("org.grails.", "datastore.gorm.validation.constraints.")
- private val after30: GrailsCommonClassNames = GrailsCommonClassNames("org.grails.", "validation.")
- private val before30: GrailsCommonClassNames = GrailsCommonClassNames("org.codehaus.groovy.grails.", "validation.")
-
- @JvmStatic
- fun getInstance(context: PsiElement): GrailsCommonClassNames {
- val app: GrailsApplication = GrailsApplicationManager.findApplication(context) ?: return before30
- if (Version.AT_LEAST_4.contains(app.grailsVersion)) {
- return after40
- }
- else if (Version.AT_LEAST_3.contains(app.grailsVersion)) {
- return after30
- }
- else {
- return before30
- }
- }
-
- private const val HIBERNATE_MAPPING_BUILDER = "orm.hibernate.cfg.HibernateMappingBuilder"
- private const val CREDIT_CARD_CONSTRAINT = "CreditCardConstraint"
- private const val MAX_SIZE_CONSTRAINT = "MaxSizeConstraint"
- private const val EMAIL_CONSTRAINT = "EmailConstraint"
- private const val BLANK_CONSTRAINT = "BlankConstraint"
- private const val RANGE_CONSTRAINT = "RangeConstraint"
- private const val URL_CONSTRAINT = "UrlConstraint"
- private const val SIZE_CONSTRAINT = "SizeConstraint"
- private const val IN_LIST_CONSTRAINT = "InListConstraint"
- private const val MATCHES_CONSTRAINT = "MatchesConstraint"
- private const val MIN_CONSTRAINT = "MinConstraint"
- private const val MAX_CONSTRAINT = "MaxConstraint"
- private const val MIN_SIZE_CONSTRAINT = "MinSizeConstraint"
- private const val SCALE_CONSTRAINT = "ScaleConstraint"
- private const val NOT_EQUAL_CONSTRAINT = "NotEqualConstraint"
- private const val NULLABLE_CONSTRAINT = "NullableConstraint"
- private const val VALIDATOR_CONSTRAINT = "ValidatorConstraint"
- private const val UNIQUE_CONSTRAINT = "UniqueConstraint"
- private const val URL_MAPPING_BUILDER = "web.mapping.DefaultUrlMappingEvaluator.UrlMappingBuilder"
- }
-
- val hibernateMappingBuilder: String = globalPrefix + HIBERNATE_MAPPING_BUILDER
- val creditCardConstraint: String = globalPrefix + constraintPath + CREDIT_CARD_CONSTRAINT
- val emailConstraint: String = globalPrefix + constraintPath + EMAIL_CONSTRAINT
- val blankConstraint: String = globalPrefix + constraintPath + BLANK_CONSTRAINT
- val rangeConstraint: String = globalPrefix + constraintPath + RANGE_CONSTRAINT
- val inListConstraint: String = globalPrefix + constraintPath + IN_LIST_CONSTRAINT
- val urlConstraint: String = globalPrefix + constraintPath + URL_CONSTRAINT
- val sizeConstraint: String = globalPrefix + constraintPath + SIZE_CONSTRAINT
- val matchesConstraint: String = globalPrefix + constraintPath + MATCHES_CONSTRAINT
- val minConstraint: String = globalPrefix + constraintPath + MIN_CONSTRAINT
- val maxConstraint: String = globalPrefix + constraintPath + MAX_CONSTRAINT
- val minSizeConstraint: String = globalPrefix + constraintPath + MIN_SIZE_CONSTRAINT
- val maxSizeConstraint: String = globalPrefix + constraintPath + MAX_SIZE_CONSTRAINT
- val scaleConstraint: String = globalPrefix + constraintPath + SCALE_CONSTRAINT
- val notEqualConstraint: String = globalPrefix + constraintPath + NOT_EQUAL_CONSTRAINT
- val nullableConstraint: String = globalPrefix + constraintPath + NULLABLE_CONSTRAINT
- val validatorConstraint: String = globalPrefix + constraintPath + VALIDATOR_CONSTRAINT
- val uniqueConstraint: String = globalPrefix + constraintPath + UNIQUE_CONSTRAINT
- val urlMappingBuilder: String = globalPrefix + URL_MAPPING_BUILDER
-
-}
\ No newline at end of file
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/OldGrailsApplication.kt b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/OldGrailsApplication.java
similarity index 62%
rename from plugin/src/main/java/org/jetbrains/plugins/grails/structure/OldGrailsApplication.kt
rename to plugin/src/main/java/org/jetbrains/plugins/grails/structure/OldGrailsApplication.java
index 11fced1..170857b 100644
--- a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/OldGrailsApplication.kt
+++ b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/OldGrailsApplication.java
@@ -13,15 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package org.jetbrains.plugins.grails.structure;
-package org.jetbrains.plugins.grails.structure
+import com.intellij.lang.properties.psi.PropertiesFile;
+import com.intellij.openapi.module.Module;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
-import com.intellij.lang.properties.psi.PropertiesFile
-import com.intellij.openapi.module.Module
+public interface OldGrailsApplication extends GrailsApplication {
-interface OldGrailsApplication : GrailsApplication {
+ @NotNull Module getModule();
- val module: Module
-
- val applicationProperties: PropertiesFile?
+ @Nullable PropertiesFile getApplicationProperties();
}
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/Grails2Application.java b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/Grails2Application.java
new file mode 100644
index 0000000..1e70256
--- /dev/null
+++ b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/Grails2Application.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.structure.impl;
+
+import com.intellij.openapi.application.ReadAction;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.plugins.grails.config.GrailsModuleStructureUtil;
+import org.jetbrains.plugins.grails.config.GrailsSettingsService;
+import org.jetbrains.plugins.grails.config.PrintGrailsSettingsConstants;
+import org.jetbrains.plugins.grails.util.version.Version;
+import org.jetbrains.plugins.grails.util.version.VersionImpl;
+
+// Public because it is referenced from the runner package. It was Kotlin `internal`, which is
+// public in bytecode, so cross-package use compiled; package-private here would not.
+@ApiStatus.Internal
+public final class Grails2Application extends OldGrailsModuleBasedApplication {
+
+ public Grails2Application(@NotNull VirtualFile root, @NotNull Module module) {
+ super(module, root);
+ }
+
+ @Override
+ public @NotNull String getName() {
+ String name = getApplicationPropertiesValue("app.name");
+ return name != null ? name : getModule().getName();
+ }
+
+ @Override
+ public @Nullable String getAppVersion() {
+ return getApplicationPropertiesValue("app.version");
+ }
+
+ public @Nullable Version getApplicationPropertiesVersion() {
+ String version = getApplicationPropertiesValue(GrailsModuleStructureUtil.GRAILS_VERSION_KEY);
+ return version == null || version.isBlank() ? null : new VersionImpl(version);
+ }
+
+ public boolean isRunForked() {
+ return getBooleanSetting(PrintGrailsSettingsConstants.DEBUG_RUN_FORK);
+ }
+
+ public boolean isTestForked() {
+ return getBooleanSetting(PrintGrailsSettingsConstants.DEBUG_TEST_FORK);
+ }
+
+ private @Nullable String getApplicationPropertiesValue(@NotNull String key) {
+ return ReadAction.compute(() -> GrailsProperties.getPropertyValue(getApplicationProperties(), key));
+ }
+
+ private boolean getBooleanSetting(@NotNull String key) {
+ return ReadAction.compute(() -> {
+ String value = GrailsSettingsService.getGrailsSettings(getModule()).properties.get(key);
+ return value != null && Boolean.parseBoolean(value);
+ });
+ }
+}
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/Grails2Application.kt b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/Grails2Application.kt
deleted file mode 100644
index 3aec612..0000000
--- a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/Grails2Application.kt
+++ /dev/null
@@ -1,46 +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.structure.impl
-
-import com.intellij.openapi.application.runReadAction
-import com.intellij.openapi.module.Module
-import com.intellij.openapi.vfs.VirtualFile
-import org.jetbrains.plugins.grails.config.GrailsModuleStructureUtil
-import org.jetbrains.plugins.grails.config.GrailsSettingsService.getGrailsSettings
-import org.jetbrains.plugins.grails.config.PrintGrailsSettingsConstants
-import org.jetbrains.plugins.grails.util.version.Version
-import org.jetbrains.plugins.grails.util.version.VersionImpl
-
-internal class Grails2Application(root: VirtualFile, module: Module) : OldGrailsModuleBasedApplication(module, root) {
-
- override val name: String get() = getApplicationPropertiesValue("app.name") ?: module.name
-
- override val appVersion: String? get() = getApplicationPropertiesValue("app.version")
-
- val applicationPropertiesVersion: Version? get() {
- val version = getApplicationPropertiesValue(GrailsModuleStructureUtil.GRAILS_VERSION_KEY)
- return if (version.isNullOrBlank()) null else VersionImpl(version)
- }
-
- val isRunForked: Boolean get() = getBooleanSetting(PrintGrailsSettingsConstants.DEBUG_RUN_FORK)
-
- val isTestForked: Boolean get() = getBooleanSetting(PrintGrailsSettingsConstants.DEBUG_TEST_FORK)
-
- private fun getApplicationPropertiesValue(key: String) = runReadAction { applicationProperties?.getPropertyValue(key) }
-
- private fun getBooleanSetting(key: String) = runReadAction { getGrailsSettings(module).properties[key]?.toBoolean() ?: false }
-}
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/Grails3ApplicationBase.java b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/Grails3ApplicationBase.java
new file mode 100644
index 0000000..5f8ec44
--- /dev/null
+++ b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/Grails3ApplicationBase.java
@@ -0,0 +1,91 @@
+/*
+ * 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.structure.impl;
+
+import com.intellij.openapi.externalSystem.model.DataNode;
+import com.intellij.openapi.externalSystem.model.project.ModuleData;
+import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.plugins.grails.gradle.GrailsModuleData;
+import org.jetbrains.plugins.grails.structure.Grails3Application;
+import org.jetbrains.plugins.grails.structure.GrailsApplicationBase;
+import org.jetbrains.plugins.grails.util.version.Version;
+import org.jetbrains.plugins.grails.util.version.VersionImpl;
+
+import java.util.Objects;
+
+abstract class Grails3ApplicationBase extends GrailsApplicationBase implements Grails3Application {
+
+ private final DataNode<ModuleData> myModuleDataNode;
+
+ // Both derived from the external-system model, which does not change for a given node, so they
+ // are computed once. Volatile publication only; the computation is idempotent.
+ private volatile GrailsModuleData myGradleData;
+ private volatile Version myGrailsVersion;
+
+ protected Grails3ApplicationBase(@NotNull Project project,
+ @NotNull VirtualFile root,
+ @NotNull DataNode<ModuleData> moduleDataNode) {
+ super(project, root);
+ myModuleDataNode = moduleDataNode;
+ }
+
+ @Override
+ public @NotNull String getName() {
+ return myModuleDataNode.getData().getExternalName();
+ }
+
+ @Override
+ public @Nullable String getAppVersion() {
+ return myModuleDataNode.getData().getVersion();
+ }
+
+ @Override
+ public @NotNull GrailsModuleData getGradleData() {
+ GrailsModuleData result = myGradleData;
+ if (result == null) {
+ result = Objects.requireNonNull(ExternalSystemApiUtil.find(myModuleDataNode, GrailsModuleData.KEY)).getData();
+ myGradleData = result;
+ }
+ return result;
+ }
+
+ @Override
+ public @NotNull Version getGrailsVersion() {
+ Version result = myGrailsVersion;
+ if (result == null) {
+ result = new VersionImpl(getGradleData().getGrailsVersion());
+ myGrailsVersion = result;
+ }
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) return true;
+ if (other == null || other.getClass() != getClass()) return false;
+ if (!super.equals(other)) return false;
+ return myModuleDataNode.equals(((Grails3ApplicationBase)other).myModuleDataNode);
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * super.hashCode() + myModuleDataNode.hashCode();
+ }
+}
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/Grails3ApplicationBase.kt b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/Grails3ApplicationBase.kt
deleted file mode 100644
index 2507f13..0000000
--- a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/Grails3ApplicationBase.kt
+++ /dev/null
@@ -1,55 +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.structure.impl
-
-import com.intellij.openapi.externalSystem.model.DataNode
-import com.intellij.openapi.externalSystem.model.project.ModuleData
-import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil
-import com.intellij.openapi.project.Project
-import com.intellij.openapi.vfs.VirtualFile
-import com.intellij.util.lazyUnsafe
-import org.jetbrains.plugins.grails.gradle.GrailsModuleData
-import org.jetbrains.plugins.grails.structure.Grails3Application
-import org.jetbrains.plugins.grails.structure.GrailsApplicationBase
-import org.jetbrains.plugins.grails.util.version.VersionImpl
-
-internal abstract class Grails3ApplicationBase(
- project: Project,
- root: VirtualFile,
- private val moduleDataNode: DataNode<ModuleData>
-) : GrailsApplicationBase(project, root), Grails3Application {
-
- override val name: String get() = moduleDataNode.data.externalName
-
- override val appVersion: String? get() = moduleDataNode.data.version
-
- override val grailsVersion by lazyUnsafe { VersionImpl(gradleData.grailsVersion) }
-
- override val gradleData by lazyUnsafe { ExternalSystemApiUtil.find(moduleDataNode, GrailsModuleData.KEY)!!.data }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other?.javaClass != javaClass) return false
- if (!super.equals(other)) return false
-
- other as Grails3ApplicationBase
-
- return moduleDataNode == other.moduleDataNode
- }
-
- override fun hashCode() = 31 * super.hashCode() + moduleDataNode.hashCode()
-}
\ No newline at end of file
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/Grails3ApplicationProvider.java b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/Grails3ApplicationProvider.java
new file mode 100644
index 0000000..5264479
--- /dev/null
+++ b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/Grails3ApplicationProvider.java
@@ -0,0 +1,64 @@
+/*
+ * 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.structure.impl;
+
+import com.intellij.openapi.externalSystem.model.DataNode;
+import com.intellij.openapi.externalSystem.model.project.ModuleData;
+import com.intellij.openapi.externalSystem.model.project.ProjectData;
+import com.intellij.openapi.externalSystem.service.project.ProjectDataManager;
+import com.intellij.openapi.externalSystem.settings.ExternalProjectSettings;
+import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil;
+import com.intellij.openapi.externalSystem.model.ExternalProjectInfo;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.ProjectRootManager;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.plugins.gradle.service.project.GradleProjectResolverUtil;
+import org.jetbrains.plugins.gradle.settings.GradleProjectSettings;
+import org.jetbrains.plugins.gradle.util.GradleConstants;
+import org.jetbrains.plugins.grails.gradle.GrailsModuleData;
+import org.jetbrains.plugins.grails.structure.GrailsApplication;
+import org.jetbrains.plugins.grails.structure.GrailsApplicationProvider;
+
+public final class Grails3ApplicationProvider extends GrailsApplicationProvider {
+
+ @Override
+ public @Nullable GrailsApplication createApplication(@NotNull Project project, @NotNull VirtualFile root) {
+ String path = root.getPath();
+
+ ExternalProjectSettings projectSettings =
+ ExternalSystemApiUtil.getSettings(project, GradleConstants.SYSTEM_ID).getLinkedProjectSettings(path);
+ if (!(projectSettings instanceof GradleProjectSettings linkedProjectSettings)) return null;
+
+ ExternalProjectInfo gradleProjectInfo = ProjectDataManager.getInstance()
+ .getExternalProjectData(project, GradleConstants.SYSTEM_ID, linkedProjectSettings.getExternalProjectPath());
+ if (gradleProjectInfo == null) return null;
+
+ DataNode<ProjectData> structure = gradleProjectInfo.getExternalProjectStructure();
+ DataNode<ModuleData> moduleData = GradleProjectResolverUtil.findModule(structure, path);
+ if (moduleData == null) return null;
+ if (ExternalSystemApiUtil.find(moduleData, GrailsModuleData.KEY) == null) return null;
+
+ if (linkedProjectSettings.isResolveModulePerSourceSet()) {
+ return new Grails3MultiModuleApplication(project, root, moduleData);
+ }
+
+ Module module = ProjectRootManager.getInstance(project).getFileIndex().getModuleForFile(root);
+ return module != null ? new Grails3SingleModuleApplication(module, root, moduleData) : null;
+ }
+}
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/Grails3ApplicationProvider.kt b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/Grails3ApplicationProvider.kt
deleted file mode 100644
index 7a490d5..0000000
--- a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/Grails3ApplicationProvider.kt
+++ /dev/null
@@ -1,54 +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.structure.impl
-
-import com.intellij.openapi.externalSystem.service.project.ProjectDataManager
-import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil
-import com.intellij.openapi.project.Project
-import com.intellij.openapi.roots.ProjectRootManager
-import com.intellij.openapi.vfs.VirtualFile
-import org.jetbrains.plugins.gradle.service.project.GradleProjectResolverUtil.findModule
-import org.jetbrains.plugins.gradle.settings.GradleProjectSettings
-import org.jetbrains.plugins.gradle.util.GradleConstants.SYSTEM_ID
-import org.jetbrains.plugins.grails.gradle.GrailsModuleData
-import org.jetbrains.plugins.grails.structure.GrailsApplication
-import org.jetbrains.plugins.grails.structure.GrailsApplicationProvider
-
-class Grails3ApplicationProvider : GrailsApplicationProvider() {
-
- override fun createApplication(project: Project, root: VirtualFile): GrailsApplication? {
- val path = root.path
-
- val settings = ExternalSystemApiUtil.getSettings(project, SYSTEM_ID)
- val linkedProjectSettings = settings.getLinkedProjectSettings(path) as? GradleProjectSettings ?: return null
- val gradleProjectInfo = ProjectDataManager.getInstance().getExternalProjectData(
- project, SYSTEM_ID, linkedProjectSettings.externalProjectPath
- ) ?: return null
-
- val moduleData = findModule(gradleProjectInfo.externalProjectStructure, path) ?: return null
- if (ExternalSystemApiUtil.find(moduleData, GrailsModuleData.KEY) == null) return null
-
- return if (linkedProjectSettings.isResolveModulePerSourceSet) {
- Grails3MultiModuleApplication(project, root, moduleData)
- }
- else {
- ProjectRootManager.getInstance(project).fileIndex.getModuleForFile(root)?.let { module ->
- Grails3SingleModuleApplication(module, root, moduleData)
- }
- }
- }
-}
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/Grails3MultiModuleApplication.java b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/Grails3MultiModuleApplication.java
new file mode 100644
index 0000000..0cd1d63
--- /dev/null
+++ b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/Grails3MultiModuleApplication.java
@@ -0,0 +1,93 @@
+/*
+ * 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.structure.impl;
+
+import com.intellij.openapi.externalSystem.model.DataNode;
+import com.intellij.openapi.externalSystem.model.project.ModuleData;
+import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.module.ModuleManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.search.GlobalSearchScope;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.plugins.gradle.model.data.GradleSourceSetData;
+
+import java.util.ArrayList;
+import java.util.List;
+
+final class Grails3MultiModuleApplication extends Grails3ApplicationBase {
+
+ private final DataNode<ModuleData> myModuleDataNode;
+
+ // The source sets come from the external-system model and do not change for a given node.
+ private volatile List<GradleSourceSetData> mySourceSets;
+
+ Grails3MultiModuleApplication(@NotNull Project project,
+ @NotNull VirtualFile root,
+ @NotNull DataNode<ModuleData> moduleDataNode) {
+ super(project, root, moduleDataNode);
+ myModuleDataNode = moduleDataNode;
+ }
+
+ @Override
+ public @NotNull GlobalSearchScope getScope(boolean includeDependencies, boolean testsOnly) {
+ if (testsOnly) {
+ return sourceSetScope("test", includeDependencies, true)
+ .uniteWith(sourceSetScope("integrationTest", includeDependencies, true));
+ }
+ return sourceSetScope("main", includeDependencies, false);
+ }
+
+ private @NotNull GlobalSearchScope sourceSetScope(@NotNull String sourceSetName,
+ boolean includeDependencies,
+ boolean testsOnly) {
+ Module module = findModule(sourceSetName);
+ if (module == null) return GlobalSearchScope.EMPTY_SCOPE;
+ return includeDependencies ? module.getModuleRuntimeScope(testsOnly) : module.getModuleScope(testsOnly);
+ }
+
+ private @NotNull List<GradleSourceSetData> getSourceSets() {
+ List<GradleSourceSetData> result = mySourceSets;
+ if (result == null) {
+ result = new ArrayList<>();
+ for (DataNode<GradleSourceSetData> node : ExternalSystemApiUtil.findAll(myModuleDataNode, GradleSourceSetData.KEY)) {
+ result.add(node.getData());
+ }
+ mySourceSets = result;
+ }
+ return result;
+ }
+
+ private @Nullable Module findModule(@NotNull String sourceSetName) {
+ GradleSourceSetData sourceSet = null;
+ for (GradleSourceSetData candidate : getSourceSets()) {
+ if (candidate.getId().endsWith(":" + sourceSetName)) {
+ sourceSet = candidate;
+ break;
+ }
+ }
+ if (sourceSet == null) return null;
+
+ for (Module module : ModuleManager.getInstance(getProject()).getModules()) {
+ if (sourceSet.getId().equals(ExternalSystemApiUtil.getExternalProjectId(module))) {
+ return module;
+ }
+ }
+ return null;
+ }
+}
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/Grails3MultiModuleApplication.kt b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/Grails3MultiModuleApplication.kt
deleted file mode 100644
index e41654b..0000000
--- a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/Grails3MultiModuleApplication.kt
+++ /dev/null
@@ -1,55 +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.structure.impl
-
-import com.intellij.openapi.externalSystem.model.DataNode
-import com.intellij.openapi.externalSystem.model.project.ModuleData
-import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil
-import com.intellij.openapi.module.Module
-import com.intellij.openapi.module.ModuleManager
-import com.intellij.openapi.project.Project
-import com.intellij.openapi.vfs.VirtualFile
-import com.intellij.psi.search.GlobalSearchScope
-import com.intellij.psi.search.GlobalSearchScope.EMPTY_SCOPE
-import org.jetbrains.plugins.gradle.model.data.GradleSourceSetData
-
-internal class Grails3MultiModuleApplication(
- project: Project,
- root: VirtualFile,
- moduleDataNode: DataNode<ModuleData>
-) : Grails3ApplicationBase(project, root, moduleDataNode) {
-
- override fun getScope(includeDependencies: Boolean, testsOnly: Boolean): GlobalSearchScope {
- fun getScope(sourceSetName: String) = findModule(sourceSetName)?.let {
- if (includeDependencies) it.getModuleRuntimeScope(testsOnly) else it.getModuleScope(testsOnly)
- } ?: EMPTY_SCOPE
-
- return when {
- testsOnly -> getScope("test").uniteWith(getScope("integrationTest"))
- else -> getScope("main")
- }
- }
-
- private val mySourceSets by lazy {
- ExternalSystemApiUtil.findAll(moduleDataNode, GradleSourceSetData.KEY).map { it.data }
- }
-
- private fun findModule(sourceSetName: String): Module? {
- val sourceSet = mySourceSets.find { it.id.endsWith(":$sourceSetName") } ?: return null
- return ModuleManager.getInstance(project).modules.find { ExternalSystemApiUtil.getExternalProjectId(it) == sourceSet.id }
- }
-}
\ No newline at end of file
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/Grails3SingleModuleApplication.java b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/Grails3SingleModuleApplication.java
new file mode 100644
index 0000000..fea9fe3
--- /dev/null
+++ b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/Grails3SingleModuleApplication.java
@@ -0,0 +1,57 @@
+/*
+ * 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.structure.impl;
+
+import com.intellij.openapi.externalSystem.model.DataNode;
+import com.intellij.openapi.externalSystem.model.project.ModuleData;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.search.GlobalSearchScope;
+import org.jetbrains.annotations.NotNull;
+
+final class Grails3SingleModuleApplication extends Grails3ApplicationBase {
+
+ private final Module myModule;
+
+ Grails3SingleModuleApplication(@NotNull Module module,
+ @NotNull VirtualFile root,
+ @NotNull DataNode<ModuleData> moduleDataNode) {
+ super(module.getProject(), root, moduleDataNode);
+ myModule = module;
+ }
+
+ public @NotNull Module getModule() {
+ return myModule;
+ }
+
+ @Override
+ public @NotNull GlobalSearchScope getScope(boolean includeDependencies, boolean testsOnly) {
+ return includeDependencies ? myModule.getModuleRuntimeScope(testsOnly) : myModule.getModuleScope(testsOnly);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) return true;
+ if (other == null || other.getClass() != getClass()) return false;
+ if (!super.equals(other)) return false;
+ return myModule.equals(((Grails3SingleModuleApplication)other).myModule);
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * super.hashCode() + myModule.hashCode();
+ }
+}
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/Grails3SingleModuleApplication.kt b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/Grails3SingleModuleApplication.kt
deleted file mode 100644
index cac4fc9..0000000
--- a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/Grails3SingleModuleApplication.kt
+++ /dev/null
@@ -1,51 +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.structure.impl
-
-import com.intellij.openapi.externalSystem.model.DataNode
-import com.intellij.openapi.externalSystem.model.project.ModuleData
-import com.intellij.openapi.module.Module
-import com.intellij.openapi.vfs.VirtualFile
-import com.intellij.psi.search.GlobalSearchScope
-
-internal class Grails3SingleModuleApplication(
- val module: Module,
- root: VirtualFile,
- moduleDataNode: DataNode<ModuleData>
-) : Grails3ApplicationBase(module.project, root, moduleDataNode) {
-
- override fun getScope(includeDependencies: Boolean, testsOnly: Boolean): GlobalSearchScope {
- return if (includeDependencies) {
- module.getModuleRuntimeScope(testsOnly)
- }
- else {
- module.getModuleScope(testsOnly)
- }
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other?.javaClass != javaClass) return false
- if (!super.equals(other)) return false
-
- other as Grails3SingleModuleApplication
-
- return module == other.module
- }
-
- override fun hashCode() = 31 * super.hashCode() + module.hashCode()
-}
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/GrailsModuleBasedApplication.java b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/GrailsModuleBasedApplication.java
new file mode 100644
index 0000000..a39f0f4
--- /dev/null
+++ b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/GrailsModuleBasedApplication.java
@@ -0,0 +1,77 @@
+/*
+ * 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.structure.impl;
+
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.roots.ProjectRootManager;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.psi.util.CachedValueProvider.Result;
+import com.intellij.psi.util.CachedValuesManager;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.plugins.grails.structure.GrailsApplicationBase;
+
+public abstract class GrailsModuleBasedApplication extends GrailsApplicationBase {
+
+ private final Module myModule;
+
+ protected GrailsModuleBasedApplication(@NotNull Module module, @NotNull VirtualFile root) {
+ super(module.getProject(), root);
+ myModule = module;
+ }
+
+ public @NotNull Module getModule() {
+ return myModule;
+ }
+
+ @Override
+ public boolean isValid() {
+ return super.isValid() && !myModule.isDisposed();
+ }
+
+ @Override
+ public @NotNull GlobalSearchScope getScope(boolean includeDependencies, boolean testsOnly) {
+ if (testsOnly && includeDependencies) {
+ return myModule.getModuleTestsWithDependentsScope();
+ }
+ if (testsOnly) {
+ // tests only, without dependencies: the test sources minus the production sources
+ return CachedValuesManager.getManager(getProject()).getCachedValue(this, () -> {
+ GlobalSearchScope moduleScope = myModule.getModuleScope(true);
+ GlobalSearchScope moduleWithoutTestsScope = myModule.getModuleScope(false);
+ GlobalSearchScope result = moduleScope.intersectWith(GlobalSearchScope.notScope(moduleWithoutTestsScope));
+ return Result.create(result, ProjectRootManager.getInstance(getProject()));
+ });
+ }
+ if (includeDependencies) {
+ return myModule.getModuleRuntimeScope(false);
+ }
+ return myModule.getModuleScope(false);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) return true;
+ if (other == null || other.getClass() != getClass()) return false;
+ if (!super.equals(other)) return false;
+ return myModule.equals(((GrailsModuleBasedApplication)other).myModule);
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * super.hashCode() + myModule.hashCode();
+ }
+}
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/GrailsModuleBasedApplication.kt b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/GrailsModuleBasedApplication.kt
deleted file mode 100644
index 4913bde..0000000
--- a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/GrailsModuleBasedApplication.kt
+++ /dev/null
@@ -1,60 +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.structure.impl
-
-import com.intellij.openapi.module.Module
-import com.intellij.openapi.roots.ProjectRootManager
-import com.intellij.openapi.vfs.VirtualFile
-import com.intellij.psi.search.GlobalSearchScope
-import com.intellij.psi.util.CachedValueProvider.Result
-import com.intellij.psi.util.CachedValuesManager
-import org.jetbrains.plugins.grails.structure.GrailsApplicationBase
-
-abstract class GrailsModuleBasedApplication(
- val module: Module,
- root: VirtualFile
-) : GrailsApplicationBase(module.project, root) {
-
- override val isValid: Boolean get() = super.isValid && !module.isDisposed
-
- override fun getScope(includeDependencies: Boolean, testsOnly: Boolean): GlobalSearchScope {
- return when {
- testsOnly && includeDependencies -> module.moduleTestsWithDependentsScope
- testsOnly && !includeDependencies -> CachedValuesManager.getManager(project).getCachedValue(this) {
- val moduleScope = module.getModuleScope(true)
- val moduleWithoutTestsScope = module.getModuleScope(false)
- val result = moduleScope.intersectWith(GlobalSearchScope.notScope(moduleWithoutTestsScope))
- Result.create(result, ProjectRootManager.getInstance(project))
- }
- !testsOnly && includeDependencies -> module.getModuleRuntimeScope(false)
- !testsOnly && !includeDependencies -> module.getModuleScope(false)
- else -> throw RuntimeException("never happens")
- }
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other?.javaClass != javaClass) return false
- if (!super.equals(other)) return false
-
- other as GrailsModuleBasedApplication
-
- return module == other.module
- }
-
- override fun hashCode(): Int = 31 * super.hashCode() + module.hashCode()
-}
\ No newline at end of file
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/GrailsProperties.java b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/GrailsProperties.java
new file mode 100644
index 0000000..2cfe544
--- /dev/null
+++ b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/GrailsProperties.java
@@ -0,0 +1,34 @@
+/*
+ * 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.structure.impl;
+
+import com.intellij.lang.properties.psi.PropertiesFile;
+import com.intellij.lang.properties.IProperty;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+final class GrailsProperties {
+
+ private GrailsProperties() {
+ }
+
+ /** Null-tolerant property lookup; was a nullable-receiver extension in impl/util.kt. */
+ static @Nullable String getPropertyValue(@Nullable PropertiesFile file, @NotNull String key) {
+ if (file == null) return null;
+ IProperty property = file.findPropertyByKey(key);
+ return property != null ? property.getValue() : null;
+ }
+}
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/OldGrailsModuleBasedApplication.java b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/OldGrailsModuleBasedApplication.java
new file mode 100644
index 0000000..72c0800
--- /dev/null
+++ b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/OldGrailsModuleBasedApplication.java
@@ -0,0 +1,81 @@
+/*
+ * 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.structure.impl;
+
+import com.intellij.lang.properties.psi.PropertiesFile;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.psi.search.GlobalSearchScopesCore;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.plugins.grails.config.GrailsConfigUtils;
+import org.jetbrains.plugins.grails.structure.OldGrailsApplication;
+import org.jetbrains.plugins.grails.util.version.Version;
+import org.jetbrains.plugins.grails.util.version.VersionImpl;
+
+public abstract class OldGrailsModuleBasedApplication extends GrailsModuleBasedApplication implements OldGrailsApplication {
+
+ private static final String[] TEST_FOLDERS = {"test/unit", "test/integration", "test/functional"};
+
+ // Deferred and cached: computing it walks the VFS, and it is only needed for tests-only scopes.
+ private volatile GlobalSearchScope myTestFoldersScope;
+
+ protected OldGrailsModuleBasedApplication(@NotNull Module module, @NotNull VirtualFile root) {
+ super(module, root);
+ }
+
+ private @NotNull GlobalSearchScope getTestFoldersScope() {
+ GlobalSearchScope result = myTestFoldersScope;
+ if (result == null) {
+ result = GlobalSearchScope.EMPTY_SCOPE;
+ for (String path : TEST_FOLDERS) {
+ VirtualFile directory = getRoot().findFileByRelativePath(path);
+ if (directory != null && directory.isDirectory()) {
+ result = result.union(GlobalSearchScopesCore.directoryScope(getProject(), directory, true));
+ }
+ }
+ myTestFoldersScope = result;
+ }
+ return result;
+ }
+
+ @Override
+ public @NotNull Version getGrailsVersion() {
+ String version = GrailsConfigUtils.getGrailsVersion(getModule());
+ return version != null ? new VersionImpl(version) : Version.LATEST_2x;
+ }
+
+ @Override
+ public @NotNull GlobalSearchScope getScope(boolean includeDependencies, boolean testsOnly) {
+ GlobalSearchScope scope = super.getScope(includeDependencies, testsOnly);
+ return testsOnly ? scope.union(getTestFoldersScope()) : scope;
+ }
+
+ @Override
+ public @Nullable PropertiesFile getApplicationProperties() {
+ VirtualFile root = getRoot();
+ if (!root.isValid()) return null;
+
+ VirtualFile applicationProperties = root.findChild("application.properties");
+ if (applicationProperties == null) return null;
+
+ PsiFile psiFile = PsiManager.getInstance(getProject()).findFile(applicationProperties);
+ return psiFile instanceof PropertiesFile propertiesFile ? propertiesFile : null;
+ }
+}
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/OldGrailsModuleBasedApplication.kt b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/OldGrailsModuleBasedApplication.kt
deleted file mode 100644
index 6ad9602..0000000
--- a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/OldGrailsModuleBasedApplication.kt
+++ /dev/null
@@ -1,69 +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.structure.impl
-
-import com.intellij.lang.properties.psi.PropertiesFile
-import com.intellij.openapi.module.Module
-import com.intellij.openapi.vfs.VirtualFile
-import com.intellij.psi.PsiManager
-import com.intellij.psi.search.GlobalSearchScope
-import com.intellij.psi.search.GlobalSearchScopesCore
-import org.jetbrains.plugins.grails.config.GrailsConfigUtils
-import org.jetbrains.plugins.grails.structure.OldGrailsApplication
-import org.jetbrains.plugins.grails.util.version.Version
-import org.jetbrains.plugins.grails.util.version.Version.LATEST_2x
-import org.jetbrains.plugins.grails.util.version.VersionImpl
-
-abstract class OldGrailsModuleBasedApplication(
- module: Module,
- root: VirtualFile
-) : GrailsModuleBasedApplication(module, root), OldGrailsApplication {
-
- private val myTestFoldersScope by lazy {
- arrayOf("test/unit", "test/integration", "test/functional").fold(GlobalSearchScope.EMPTY_SCOPE) { scope, path ->
- val directory = root.findFileByRelativePath(path)
- if (directory != null && directory.isDirectory) {
- scope.union(GlobalSearchScopesCore.directoryScope(project, directory, true))
- }
- else {
- scope
- }
- }
- }
-
- override val grailsVersion: Version get() = GrailsConfigUtils.getGrailsVersion(module)?.let { VersionImpl(it) } ?: LATEST_2x
-
- override fun getScope(includeDependencies: Boolean, testsOnly: Boolean): GlobalSearchScope {
- val scope = super.getScope(includeDependencies, testsOnly)
- if (testsOnly) {
- return scope.union(myTestFoldersScope)
- }
- else {
- return scope
- }
- }
-
- override val applicationProperties: PropertiesFile? get() {
- val root = root
- if (!root.isValid) return null
-
- val applicationProperties = root.findChild("application.properties") ?: return null
-
- val psiFile = PsiManager.getInstance(project).findFile(applicationProperties)
- return if (psiFile is PropertiesFile) psiFile else null
- }
-}
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/util.kt b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/util.kt
deleted file mode 100644
index 186f511..0000000
--- a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/impl/util.kt
+++ /dev/null
@@ -1,21 +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.structure.impl
-
-import com.intellij.lang.properties.psi.PropertiesFile
-
-internal fun PropertiesFile?.getPropertyValue(key: String) = this?.findPropertyByKey(key)?.value
diff --git a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/util.kt b/plugin/src/main/java/org/jetbrains/plugins/grails/structure/util.kt
deleted file mode 100644
index fe5de44..0000000
--- a/plugin/src/main/java/org/jetbrains/plugins/grails/structure/util.kt
+++ /dev/null
@@ -1,27 +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.structure
-
-@JvmField
-val COMPARATOR: Comparator<GrailsApplication?> = Comparator { app1, app2 ->
- when {
- app1 == null && app2 == null -> 0
- app1 == null -> -1
- app2 == null -> 1
- else -> app1.name.compareTo(app2.name, ignoreCase = true)
- }
-}
\ No newline at end of file