diff --git a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/sitemesh3/Sitemesh3.java b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/sitemesh3/Sitemesh3.java index 73fd1dc66a3..7d5fcddb2fd 100644 --- a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/sitemesh3/Sitemesh3.java +++ b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/sitemesh3/Sitemesh3.java @@ -22,14 +22,20 @@ import org.grails.forge.application.ApplicationType; import org.grails.forge.application.generator.GeneratorContext; import org.grails.forge.build.dependencies.Dependency; -import org.grails.forge.feature.Category; +import org.grails.forge.feature.DefaultFeature; import org.grails.forge.feature.Feature; +import org.grails.forge.feature.view.GspLayout; +import org.grails.forge.options.Options; -@Singleton -public class Sitemesh3 implements Feature { +import java.util.Set; - public Sitemesh3() { - } +/** + * Default GSP layout decorator, backed by SiteMesh 3 ({@code grails-sitemesh3}). + * Applied automatically to web applications unless another {@link GspLayout} + * (e.g. {@code grails-layout}) is explicitly selected. + */ +@Singleton +public class Sitemesh3 extends GspLayout implements DefaultFeature { @Override public String getName() { @@ -38,12 +44,18 @@ public String getName() { @Override public String getTitle() { - return "Sitemesh 3"; + return "SiteMesh 3"; } @Override public String getDescription() { - return "Adds support for Sitemesh3 based layouts instead of Sitemesh 2"; + return "Adds support for SiteMesh 3 based GSP layouts"; + } + + @Override + public boolean shouldApply(ApplicationType applicationType, Options options, Set selectedFeatures) { + return supports(applicationType) && + selectedFeatures.stream().noneMatch(GspLayout.class::isInstance); } @Override @@ -53,14 +65,4 @@ public void apply(GeneratorContext generatorContext) { .artifactId("grails-sitemesh3") .implementation()); } - - @Override - public boolean supports(ApplicationType applicationType) { - return true; - } - - @Override - public String getCategory() { - return Category.VIEW; - } } diff --git a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/view/GrailsLayout.java b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/view/GrailsLayout.java new file mode 100644 index 00000000000..5bd2b958b9c --- /dev/null +++ b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/view/GrailsLayout.java @@ -0,0 +1,55 @@ +/* + * 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 org.grails.forge.feature.view; + +import jakarta.inject.Singleton; +import org.grails.forge.application.generator.GeneratorContext; +import org.grails.forge.build.dependencies.Dependency; + +/** + * Opt-in GSP layout decorator backed by the legacy SiteMesh 2 based + * {@code grails-layout} plugin. Mutually exclusive with {@code sitemesh3}; + * selecting this feature replaces the default SiteMesh 3 decorator. + */ +@Singleton +public class GrailsLayout extends GspLayout { + + @Override + public String getName() { + return "grails-layout"; + } + + @Override + public String getTitle() { + return "GSP SiteMesh 2 Layouts"; + } + + @Override + public String getDescription() { + return "Adds support for legacy SiteMesh 2 based GSP layouts (grails-layout) instead of SiteMesh 3"; + } + + @Override + public void apply(GeneratorContext generatorContext) { + generatorContext.addDependency(Dependency.builder() + .groupId("org.apache.grails") + .artifactId("grails-layout") + .implementation()); + } +} diff --git a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/view/GspLayout.java b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/view/GspLayout.java new file mode 100644 index 00000000000..d818518645f --- /dev/null +++ b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/view/GspLayout.java @@ -0,0 +1,47 @@ +/* + * 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 org.grails.forge.feature.view; + +import org.grails.forge.application.ApplicationType; +import org.grails.forge.feature.Category; +import org.grails.forge.feature.OneOfFeature; + +/** + * Common parent for the mutually exclusive GSP layout decorators: SiteMesh 3 + * ({@code grails-sitemesh3}) and the legacy SiteMesh 2 based {@code grails-layout}. + * Because they share the same {@link #getFeatureClass()}, only one of them may be + * selected for a given application (enforced by the one-of feature validator). + */ +public abstract class GspLayout implements OneOfFeature { + + @Override + public Class getFeatureClass() { + return GspLayout.class; + } + + @Override + public boolean supports(ApplicationType applicationType) { + return applicationType == ApplicationType.WEB || applicationType == ApplicationType.WEB_PLUGIN; + } + + @Override + public String getCategory() { + return Category.VIEW; + } +} diff --git a/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/view/GspLayoutSpec.groovy b/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/view/GspLayoutSpec.groovy new file mode 100644 index 00000000000..3ddadd80582 --- /dev/null +++ b/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/view/GspLayoutSpec.groovy @@ -0,0 +1,72 @@ +/* + * 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 org.grails.forge.feature.view + +import org.grails.forge.ApplicationContextSpec +import org.grails.forge.BuildBuilder +import org.grails.forge.application.ApplicationType +import spock.lang.Unroll + +class GspLayoutSpec extends ApplicationContextSpec { + + @Unroll + void "web #applicationType apps use SiteMesh 3 (grails-sitemesh3) by default"() { + when: + final String build = new BuildBuilder(beanContext) + .features(["gsp"]) + .applicationType(applicationType) + .render() + + then: + build.contains('implementation "org.apache.grails:grails-sitemesh3"') + !build.contains('implementation "org.apache.grails:grails-layout"') + + and: "a SNAPSHOT build exposes the SiteMesh 3 snapshot repo so org.sitemesh:*-SNAPSHOT resolves" + build.contains("https://central.sonatype.com/repository/maven-snapshots") + build.contains("includeVersionByRegex('org[.]sitemesh.*'") + + where: + applicationType << [ApplicationType.WEB, ApplicationType.WEB_PLUGIN] + } + + void "selecting grails-layout replaces the default SiteMesh 3 decorator"() { + when: + final String build = new BuildBuilder(beanContext) + .features(["gsp", "grails-layout"]) + .applicationType(ApplicationType.WEB) + .render() + + then: + build.contains('implementation "org.apache.grails:grails-layout"') + !build.contains('implementation "org.apache.grails:grails-sitemesh3"') + } + + void "sitemesh3 and grails-layout cannot both be selected"() { + when: + new BuildBuilder(beanContext) + .features(["gsp", "sitemesh3", "grails-layout"]) + .applicationType(ApplicationType.WEB) + .render() + + then: + IllegalArgumentException e = thrown() + e.message.contains("There can only be one of the following features selected") + } +}