codeconsole commented on code in PR #16385:
URL: https://github.com/apache/grails-core/pull/16385#discussion_r4086804784


##########
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/scaffolding/GenerateScaffoldedViewsTask.groovy:
##########
@@ -189,39 +200,75 @@ abstract class GenerateScaffoldedViewsTask extends 
DefaultTask {
         templates
     }
 
+    /** Compiled plugin pages win over runtime scaffolding, so they must also 
win at build time. */
+    private Set<String> findPluginViews() {
+        Set<String> views = []
+        for (File entry : viewClasspath.files) {
+            Properties index = new Properties()
+            if (entry.isDirectory()) {
+                File resource = new File(entry, 'gsp/views.properties')
+                if (resource.isFile()) {
+                    resource.withInputStream { InputStream input -> 
index.load(input) }
+                }
+            }
+            else if (entry.name.endsWith('.jar') && entry.isFile()) {
+                new JarFile(entry).withCloseable { JarFile jar ->
+                    JarEntry resource = jar.getJarEntry('gsp/views.properties')
+                    if (resource != null) {
+                        jar.getInputStream(resource).withCloseable { 
InputStream input -> index.load(input) }
+                    }
+                }
+            }
+            views.addAll(index.stringPropertyNames())
+        }
+        views
+    }
+
     /**
      * Maps view directory name to the fully qualified domain class, for every 
{@code @Scaffold}
      * controller. Qualified rather than simple because a view declaring the 
type of its model has to
      * name a type that resolves.
      *
-     * <p>A view directory is named for the controller alone - {@code 
getDeployedViewURI} builds
-     * {@code /WEB-INF/grails-app/views/<controller>/<view>.gsp} and never 
consults the namespace -
-     * so two controllers of the same simple name in different packages share 
one directory whatever
-     * their namespaces are. Where they scaffold different domains, no single 
page can serve both:
-     * whichever was written would declare one domain as its model and be 
rendered by the controller
-     * of the other. Both are left out rather than one of them guessed at, and 
the resolver goes on
-     * expanding a template per request for them, which is what it did before 
any of this and is the
-     * one thing that gets each controller its own domain. Everything else in 
the project is still
-     * precompiled.</p>
+     * <p>Namespaced controllers are left to the runtime resolver, which can 
evaluate the namespace
+     * and select namespace-specific templates. Emitting their pages into a 
shared, unqualified
+     * directory would make them visible to unrelated controllers. The entire 
shared directory is
+     * left out, including when an unqualified controller also claims it.</p>
+     *
+     * <p>Likewise, controllers sharing a name but scaffolding different 
domains cannot share a
+     * precompiled page. The runtime resolver expands a template for the 
appropriate domain.</p>
      */
     private Map<String, String> findScaffoldedControllers() {
         Map<String, String> found = [:]
         Map<String, List<String>> claimants = [:]
-        for (File dir : classesDirs.files) {
-            if (!dir.isDirectory()) {
-                continue
-            }
-            dir.eachFileRecurse { File f ->
-                if (!f.name.endsWith('Controller.class')) {
-                    return
+        Set<String> namespaced = []
+        URL[] classpath = (classesDirs.files + 
templateClasspath.files).collect { it.toURI().toURL() } as URL[]
+        new URLClassLoader(classpath, (ClassLoader) null).withCloseable { 
URLClassLoader resources ->
+            for (File dir : classesDirs.files) {
+                if (!dir.isDirectory()) {
+                    continue
                 }
-                String domain = readScaffoldDomain(f)
-                if (domain == null) {
-                    return
+                dir.eachFileRecurse { File f ->
+                    if (!f.name.endsWith('Controller.class')) {
+                        return
+                    }
+                    String controllerName = decapitalize(f.name - 
'Controller.class')

Review Comment:
   Fixed in 9f945f3ab0. The view directory is now derived as the runtime 
derives it, `getPropertyNameRepresentation(getLogicalName(name, 
'Controller'))`, so `APIController` writes `API/` and both precedence checks 
key on `API/<view>.gsp`. The domain's `propertyName` binding had the same bug 
(`URLMapping` came out as `uRLMapping`); it now goes through 
`GrailsNameUtils.getPropertyName`, matching `ModelBuilder`. New specs cover the 
directory, the binding, and both the application-view and plugin-index checks 
for an acronym controller. They assert on the listed directory name, because a 
case-insensitive file system resolves `aPI` to `API` by lookup.



##########
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/scaffolding/GenerateScaffoldedViewsTask.groovy:
##########
@@ -128,7 +135,11 @@ abstract class GenerateScaffoldedViewsTask extends 
DefaultTask {
                 // a view the application wrote itself already wins at 
runtime, so leaving it out
                 // keeps build-time and runtime resolution agreeing
                 if (declared.any { 
it.path.endsWith("views/${controller.key}/${viewName}.gsp".toString()) }) {
-                    logger.info("Skipping ${controller.key}/${viewName}.gsp, 
the application declares it")
+                    logger.info('Skipping {}/{}.gsp, the application declares 
it', controller.key, viewName)
+                    continue
+                }
+                if 
(pluginViews.contains("/WEB-INF/grails-app/views/${controller.key}/${viewName}.gsp".toString()))
 {

Review Comment:
   Fixed in a4e129569c. `GroovyPagePlugin.VIEWS_SERVER_PATH` is now the value 
`compileGroovyPages` is given for `serverpath` and the prefix the task matches 
index keys against, so the two can't drift apart.



##########
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/scaffolding/GenerateScaffoldedViewsTask.groovy:
##########
@@ -189,39 +200,75 @@ abstract class GenerateScaffoldedViewsTask extends 
DefaultTask {
         templates
     }
 
+    /** Compiled plugin pages win over runtime scaffolding, so they must also 
win at build time. */
+    private Set<String> findPluginViews() {
+        Set<String> views = []
+        for (File entry : viewClasspath.files) {
+            Properties index = new Properties()
+            if (entry.isDirectory()) {
+                File resource = new File(entry, 'gsp/views.properties')

Review Comment:
   Fixed in a4e129569c. The task now probes `META-INF/views.properties` first 
and falls back to `gsp/views.properties`, taking one index per artifact as 
`initializeViewMap` does. Specs cover the descriptor-relative index in a 
directory and in a jar, plus a plugin carrying both, where only the first is 
honoured.



##########
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/scaffolding/GenerateScaffoldedViewsTask.groovy:
##########
@@ -189,39 +200,75 @@ abstract class GenerateScaffoldedViewsTask extends 
DefaultTask {
         templates
     }
 
+    /** Compiled plugin pages win over runtime scaffolding, so they must also 
win at build time. */
+    private Set<String> findPluginViews() {
+        Set<String> views = []
+        for (File entry : viewClasspath.files) {
+            Properties index = new Properties()
+            if (entry.isDirectory()) {
+                File resource = new File(entry, 'gsp/views.properties')
+                if (resource.isFile()) {
+                    resource.withInputStream { InputStream input -> 
index.load(input) }
+                }
+            }
+            else if (entry.name.endsWith('.jar') && entry.isFile()) {
+                new JarFile(entry).withCloseable { JarFile jar ->
+                    JarEntry resource = jar.getJarEntry('gsp/views.properties')
+                    if (resource != null) {
+                        jar.getInputStream(resource).withCloseable { 
InputStream input -> index.load(input) }
+                    }
+                }
+            }
+            views.addAll(index.stringPropertyNames())
+        }
+        views
+    }
+
     /**
      * Maps view directory name to the fully qualified domain class, for every 
{@code @Scaffold}
      * controller. Qualified rather than simple because a view declaring the 
type of its model has to
      * name a type that resolves.
      *
-     * <p>A view directory is named for the controller alone - {@code 
getDeployedViewURI} builds
-     * {@code /WEB-INF/grails-app/views/<controller>/<view>.gsp} and never 
consults the namespace -
-     * so two controllers of the same simple name in different packages share 
one directory whatever
-     * their namespaces are. Where they scaffold different domains, no single 
page can serve both:
-     * whichever was written would declare one domain as its model and be 
rendered by the controller
-     * of the other. Both are left out rather than one of them guessed at, and 
the resolver goes on
-     * expanding a template per request for them, which is what it did before 
any of this and is the
-     * one thing that gets each controller its own domain. Everything else in 
the project is still
-     * precompiled.</p>
+     * <p>Namespaced controllers are left to the runtime resolver, which can 
evaluate the namespace
+     * and select namespace-specific templates. Emitting their pages into a 
shared, unqualified
+     * directory would make them visible to unrelated controllers. The entire 
shared directory is
+     * left out, including when an unqualified controller also claims it.</p>

Review Comment:
   I kept the blanket rule and documented it in 68af07731d. The narrowing would 
need to know whether `<ns>/<view>.gsp` exists, and the build can't answer that. 
Besides the value being in `<clinit>`, the runtime also finds namespace 
templates in places this task doesn't read: the application's own 
`META-INF/templates/scaffolding/<ns>/` resources beside the controller class, 
`src/main/templates/scaffolding/<ns>/` in development, and a template-override 
plugin. Getting it wrong would silently precompile a plain page over a 
namespace-specific one, so the javadoc now explains the trade-off.



##########
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/scaffolding/GenerateScaffoldedViewsTask.groovy:
##########
@@ -189,39 +200,75 @@ abstract class GenerateScaffoldedViewsTask extends 
DefaultTask {
         templates
     }
 
+    /** Compiled plugin pages win over runtime scaffolding, so they must also 
win at build time. */
+    private Set<String> findPluginViews() {
+        Set<String> views = []
+        for (File entry : viewClasspath.files) {
+            Properties index = new Properties()
+            if (entry.isDirectory()) {
+                File resource = new File(entry, 'gsp/views.properties')
+                if (resource.isFile()) {
+                    resource.withInputStream { InputStream input -> 
index.load(input) }
+                }
+            }
+            else if (entry.name.endsWith('.jar') && entry.isFile()) {
+                new JarFile(entry).withCloseable { JarFile jar ->
+                    JarEntry resource = jar.getJarEntry('gsp/views.properties')
+                    if (resource != null) {
+                        jar.getInputStream(resource).withCloseable { 
InputStream input -> index.load(input) }
+                    }
+                }
+            }
+            views.addAll(index.stringPropertyNames())
+        }
+        views
+    }
+
     /**
      * Maps view directory name to the fully qualified domain class, for every 
{@code @Scaffold}
      * controller. Qualified rather than simple because a view declaring the 
type of its model has to
      * name a type that resolves.
      *
-     * <p>A view directory is named for the controller alone - {@code 
getDeployedViewURI} builds
-     * {@code /WEB-INF/grails-app/views/<controller>/<view>.gsp} and never 
consults the namespace -
-     * so two controllers of the same simple name in different packages share 
one directory whatever
-     * their namespaces are. Where they scaffold different domains, no single 
page can serve both:
-     * whichever was written would declare one domain as its model and be 
rendered by the controller
-     * of the other. Both are left out rather than one of them guessed at, and 
the resolver goes on
-     * expanding a template per request for them, which is what it did before 
any of this and is the
-     * one thing that gets each controller its own domain. Everything else in 
the project is still
-     * precompiled.</p>
+     * <p>Namespaced controllers are left to the runtime resolver, which can 
evaluate the namespace
+     * and select namespace-specific templates. Emitting their pages into a 
shared, unqualified
+     * directory would make them visible to unrelated controllers. The entire 
shared directory is
+     * left out, including when an unqualified controller also claims it.</p>
+     *
+     * <p>Likewise, controllers sharing a name but scaffolding different 
domains cannot share a
+     * precompiled page. The runtime resolver expands a template for the 
appropriate domain.</p>
      */
     private Map<String, String> findScaffoldedControllers() {
         Map<String, String> found = [:]
         Map<String, List<String>> claimants = [:]
-        for (File dir : classesDirs.files) {
-            if (!dir.isDirectory()) {
-                continue
-            }
-            dir.eachFileRecurse { File f ->
-                if (!f.name.endsWith('Controller.class')) {
-                    return
+        Set<String> namespaced = []
+        URL[] classpath = (classesDirs.files + 
templateClasspath.files).collect { it.toURI().toURL() } as URL[]

Review Comment:
   Fixed in 68af07731d. There's a separate `@Classpath controllerClasspath`, 
wired from `compileClasspath`, used only to resolve superclasses. The 
inherited-from-jar spec now uses it, and a new spec checks that a base class 
reachable only on `templateClasspath` is *not* searched, which pins the 
independence.



##########
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/scaffolding/GenerateScaffoldedViewsTask.groovy:
##########
@@ -237,6 +284,33 @@ abstract class GenerateScaffoldedViewsTask extends 
DefaultTask {
         found
     }
 
+    /** Read declarations, including inherited ones, without evaluating 
application code. */
+    private boolean hasNamespace(ClassReader reader, ClassLoader resources) {
+        boolean declared = false
+        reader.accept(new ClassVisitor(Opcodes.ASM9) {
+            @Override
+            FieldVisitor visitField(int access, String name, String 
descriptor, String signature, Object value) {
+                if (name == 'namespace' && (access & Opcodes.ACC_STATIC) != 0) 
{

Review Comment:
   Added to the `hasNamespace` javadoc in 68af07731d: only the declaration is 
visible, not its value, so `static namespace = null` still counts. The cost is 
runtime expansion rather than precompilation.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to