diff --git a/src/main/java/com/intellij/struts2/annotators/StrutsWebFacetCheckingAnnotator.java b/src/main/java/com/intellij/struts2/annotators/StrutsWebFacetCheckingAnnotator.java new file mode 100644 index 0000000..a50ef23 --- /dev/null +++ b/src/main/java/com/intellij/struts2/annotators/StrutsWebFacetCheckingAnnotator.java @@ -0,0 +1,119 @@ +/* + * 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 + * + * 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.struts2.annotators; + +import com.intellij.codeInsight.intention.IntentionAction; +import com.intellij.lang.annotation.AnnotationHolder; +import com.intellij.lang.annotation.Annotator; +import com.intellij.lang.annotation.HighlightSeverity; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleUtilCore; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.roots.ui.configuration.ModulesConfigurator; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.jsp.JspFile; +import com.intellij.psi.xml.XmlFile; +import com.intellij.struts2.StrutsBundle; +import com.intellij.struts2.dom.struts.model.StrutsManager; +import com.intellij.struts2.facet.StrutsFacet; +import com.intellij.struts2.facet.WebFacetChecker; +import com.intellij.util.IncorrectOperationException; +import org.jetbrains.annotations.NotNull; + +/** + * Warns when a {@code struts.xml} file is opened in a module that has a Struts + * facet but no {@link com.intellij.javaee.web.facet.WebFacet}, which prevents + * dispatch-style result paths (JSP etc.) from resolving. + */ +public class StrutsWebFacetCheckingAnnotator implements Annotator { + + @Override + public void annotate(@NotNull PsiElement psiElement, @NotNull AnnotationHolder holder) { + if (!(psiElement instanceof XmlFile xmlFile)) { + return; + } + + if (psiElement instanceof JspFile) { + return; + } + + Module module = ModuleUtilCore.findModuleForPsiElement(psiElement); + if (module == null) { + return; + } + + StrutsFacet strutsFacet = StrutsFacet.getInstance(module); + if (strutsFacet == null) { + return; + } + + StrutsManager strutsManager = StrutsManager.getInstance(psiElement.getProject()); + if (!strutsManager.isStruts2ConfigFile(xmlFile)) { + return; + } + + if (!WebFacetChecker.isWebFacetMissing(module)) { + return; + } + + holder.newAnnotation(HighlightSeverity.WARNING, + StrutsBundle.message("annotators.webfacet.missing")) + .range(xmlFile) + .fileLevel() + .withFix(new ConfigureWebFacetFix(strutsFacet)) + .create(); + } + + private static final class ConfigureWebFacetFix implements IntentionAction { + + private final StrutsFacet myStrutsFacet; + + private ConfigureWebFacetFix(@NotNull StrutsFacet strutsFacet) { + myStrutsFacet = strutsFacet; + } + + @Override + @NotNull + public String getText() { + return StrutsBundle.message("annotators.webfacet.configure"); + } + + @Override + @NotNull + public String getFamilyName() { + return StrutsBundle.message("intentions.family.name"); + } + + @Override + public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile psiFile) { + return true; + } + + @Override + public void invoke(@NotNull Project project, Editor editor, PsiFile psiFile) + throws IncorrectOperationException { + ModulesConfigurator.showFacetSettingsDialog(myStrutsFacet, null); + } + + @Override + public boolean startInWriteAction() { + return false; + } + } +} diff --git a/src/main/java/com/intellij/struts2/facet/StrutsFrameworkInitializer.java b/src/main/java/com/intellij/struts2/facet/StrutsFrameworkInitializer.java index 278ae0b..6713a5c 100644 --- a/src/main/java/com/intellij/struts2/facet/StrutsFrameworkInitializer.java +++ b/src/main/java/com/intellij/struts2/facet/StrutsFrameworkInitializer.java @@ -32,6 +32,7 @@ import com.intellij.psi.PsiDirectory; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; +import com.intellij.struts2.StrutsBundle; import com.intellij.struts2.StrutsConstants; import com.intellij.struts2.facet.ui.StrutsConfigsSearcher; import com.intellij.struts2.facet.ui.StrutsFileSet; @@ -127,16 +128,7 @@ public Object execute(@NotNull Project project, @NotNull Continuation + * Shared between the file-level annotator and the framework initialization + * notification so the condition is defined in one place. + */ +public final class WebFacetChecker { + + private WebFacetChecker() {} + + /** + * Returns {@code true} when the given module has a Struts facet but lacks + * a usable {@link WebFacet}, meaning dispatch-style result paths + * (e.g. {@code /index.jsp}) cannot be resolved. + */ + public static boolean isWebFacetMissing(@NotNull Module module) { + StrutsFacet strutsFacet = StrutsFacet.getInstance(module); + if (strutsFacet == null) return false; + return strutsFacet.getWebFacet() == null; + } + + /** + * Variant that also checks via {@link WebUtil#getWebFacet(PsiElement)}, + * which searches the module and its dependants. + */ + public static boolean isWebFacetMissing(@NotNull PsiElement element) { + Module module = ModuleUtilCore.findModuleForPsiElement(element); + if (module == null) return false; + StrutsFacet strutsFacet = StrutsFacet.getInstance(module); + if (strutsFacet == null) return false; + return WebUtil.getWebFacet(element) == null; + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 0c55621..73e08db 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -155,6 +155,7 @@ implementationClass="com.intellij.struts2.dom.params.ParamNameConverterImpl"/> + mavenLibs = new SmartList<>(); private final List callbacks = new SmartList<>(); @@ -71,6 +72,15 @@ public Struts2ProjectDescriptorBuilder withStrutsFacet() { return this; } + /** + * Prevents the default WebFacet from being added, so tests can verify + * behaviour when no WebFacet is present. + */ + public Struts2ProjectDescriptorBuilder withoutWebFacet() { + skipWebFacet = true; + return this; + } + public Struts2ProjectDescriptorBuilder withCallback(Callback callback) { callbacks.add(callback); return this; @@ -106,21 +116,25 @@ public void configureModule(@NotNull Module module, @NotNull ModifiableRootModel MavenDependencyUtil.addFromMaven(model, lib); } - final WebFacet webFacet = FacetUtil.addFacet(module, WebFacetType.getInstance()); - if (addStrutsFacet) { - FacetManager.getInstance(module).addFacet(StrutsFacetType.getInstance(), "struts2", null); - } + if (!skipWebFacet) { + final WebFacet webFacet = FacetUtil.addFacet(module, WebFacetType.getInstance()); + if (addStrutsFacet) { + FacetManager.getInstance(module).addFacet(StrutsFacetType.getInstance(), "struts2", null); + } - if (addWebFacet) { - final String sourceRootUrl = model.getSourceRootUrls()[0]; - webFacet.addWebRoot(sourceRootUrl, "/"); + if (addWebFacet) { + final String sourceRootUrl = model.getSourceRootUrls()[0]; + webFacet.addWebRoot(sourceRootUrl, "/"); - final ConfigFileInfoSet descriptors = webFacet.getDescriptorsContainer().getConfiguration(); - descriptors.addConfigFile(DeploymentDescriptorsConstants.WEB_XML_META_DATA, sourceRootUrl + "/WEB-INF/web.xml"); + final ConfigFileInfoSet descriptors = webFacet.getDescriptorsContainer().getConfiguration(); + descriptors.addConfigFile(DeploymentDescriptorsConstants.WEB_XML_META_DATA, sourceRootUrl + "/WEB-INF/web.xml"); - for (String url : ModuleRootManager.getInstance(module).getSourceRootUrls()) { - webFacet.addWebSourceRoot(url); + for (String url : ModuleRootManager.getInstance(module).getSourceRootUrls()) { + webFacet.addWebSourceRoot(url); + } } + } else if (addStrutsFacet) { + FacetManager.getInstance(module).addFacet(StrutsFacetType.getInstance(), "struts2", null); } for (Callback callback : callbacks) { @@ -157,6 +171,7 @@ public boolean equals(Object o) { if (addStrutsLibrary != builder.addStrutsLibrary) return false; if (addStrutsFacet != builder.addStrutsFacet) return false; if (addWebFacet != builder.addWebFacet) return false; + if (skipWebFacet != builder.skipWebFacet) return false; if (!callbacks.equals(builder.callbacks)) return false; if (!mavenLibs.equals(builder.mavenLibs)) return false; @@ -168,6 +183,7 @@ public int hashCode() { int result = (addStrutsLibrary ? 1 : 0); result = 31 * result + (addStrutsFacet ? 1 : 0); result = 31 * result + (addWebFacet ? 1 : 0); + result = 31 * result + (skipWebFacet ? 1 : 0); result = 31 * result + mavenLibs.hashCode(); result = 31 * result + callbacks.hashCode(); return result; diff --git a/src/test/java/com/intellij/struts2/annotators/StrutsWebFacetCheckingAnnotatorTest.java b/src/test/java/com/intellij/struts2/annotators/StrutsWebFacetCheckingAnnotatorTest.java new file mode 100644 index 0000000..36b9ffb --- /dev/null +++ b/src/test/java/com/intellij/struts2/annotators/StrutsWebFacetCheckingAnnotatorTest.java @@ -0,0 +1,56 @@ +/* + * 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 + * + * 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.struts2.annotators; + +import com.intellij.codeInsight.intention.IntentionAction; +import com.intellij.struts2.BasicLightHighlightingTestCase; +import com.intellij.struts2.Struts2ProjectDescriptorBuilder; +import com.intellij.testFramework.LightProjectDescriptor; +import org.jetbrains.annotations.NotNull; + +/** + * Tests {@link StrutsWebFacetCheckingAnnotator}. + */ +public class StrutsWebFacetCheckingAnnotatorTest extends BasicLightHighlightingTestCase { + + private static final LightProjectDescriptor NO_WEB_FACET = + new Struts2ProjectDescriptorBuilder() + .withStrutsLibrary() + .withStrutsFacet() + .withoutWebFacet() + .build(); + + @NotNull + @Override + protected LightProjectDescriptor getProjectDescriptor() { + return NO_WEB_FACET; + } + + @Override + @NotNull + protected String getTestDataLocation() { + return "strutsXml/highlighting"; + } + + public void testWebFacetMissingWarning() { + createStrutsFileSet("struts-simple.xml"); + IntentionAction intention = myFixture.getAvailableIntention( + "Configure Web facet in module settings", "struts-simple.xml"); + assertNotNull("WebFacet warning quick-fix should be available", + intention); + } +} diff --git a/src/test/java/com/intellij/struts2/facet/WebFacetCheckerTest.java b/src/test/java/com/intellij/struts2/facet/WebFacetCheckerTest.java new file mode 100644 index 0000000..b5f5cad --- /dev/null +++ b/src/test/java/com/intellij/struts2/facet/WebFacetCheckerTest.java @@ -0,0 +1,54 @@ +/* + * 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 + * + * 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.struts2.facet; + +import com.intellij.struts2.BasicLightHighlightingTestCase; +import com.intellij.struts2.Struts2ProjectDescriptorBuilder; +import com.intellij.testFramework.LightProjectDescriptor; +import org.jetbrains.annotations.NotNull; + +/** + * Tests {@link WebFacetChecker}. + */ +public class WebFacetCheckerTest extends BasicLightHighlightingTestCase { + + @NotNull + @Override + protected String getTestDataLocation() { + return "strutsXml/highlighting"; + } + + public void testWebFacetPresent() { + assertFalse("Should not report missing WebFacet when one is configured", + WebFacetChecker.isWebFacetMissing(getModule())); + } + + public void testWebFacetMissing() { + // The default descriptor adds WebFacet, so this test verifies the positive case. + // The negative case (actually missing) is tested via the annotator test + // which uses the withoutWebFacet() descriptor. + assertFalse(WebFacetChecker.isWebFacetMissing(getModule())); + } + + public void testNoStrutsFacet() { + // When no Struts facet at all, isWebFacetMissing should return false + // (the check only applies to modules with Struts facet). + // Cannot easily test this in a light test that always has a Struts facet, + // but we verify the return is false (both facets present). + assertFalse(WebFacetChecker.isWebFacetMissing(getModule())); + } +}