codeconsole commented on code in PR #16398: URL: https://github.com/apache/grails-core/pull/16398#discussion_r4101661038
########## grails-scaffolding/src/main/groovy/org/apache/grails/scaffolding/ScaffoldedPagesGenerator.groovy: ########## @@ -0,0 +1,139 @@ +/* + * 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.apache.grails.scaffolding + +import groovy.transform.CompileStatic + +import grails.codegen.model.ModelBuilder + +/** + * Expands scaffolding templates into the pages the build compiles, run by the Gradle plugin's + * {@code generateScaffoldedViews} task in a JVM on the application's own classpath. + * + * <p>Running here rather than in the build means a page is modelled by the application's + * {@link ModelBuilder}, expanded by the application's Groovy and named by {@link ScaffoldedPages}, + * exactly as the resolver models, expands and names it when the view is asked for.</p> + * + * <pre> + * ScaffoldedPagesGenerator <plan> <origins> <output directory> <page encoding> + * </pre> + * + * <p>Each line of the plan names a domain class and, tab separated, the templates directories to + * expand for it. A templates directory holds a file per template path, such as {@code show.gsp} or + * {@code admin/show.gsp}. Each line of the origins names a templates directory and, after a tab, + * where its template came from. The pages are written in the encoding they will be compiled + * with.</p> + * + * @since 8.0 + */ +@CompileStatic +class ScaffoldedPagesGenerator implements ModelBuilder { + + static void main(String[] args) { + if (args.length != 4) { + System.err.println('Usage: ScaffoldedPagesGenerator <plan> <origins> <output directory> <page encoding>') + System.exit(2) + } + Map<String, List<File>> plan = [:] + new File(args[0]).readLines('UTF-8').each { String line -> + List<String> fields = line.split('\t').toList()*.trim().findAll { String field -> field } + if (fields) { + plan.put(fields.head(), fields.tail().collect { String dir -> new File(dir) }) + } + } + Map<File, String> origins = [:] + new File(args[1]).readLines('UTF-8').each { String line -> + int tab = line.indexOf('\t') + if (tab > 0) { + origins.put(new File(line.substring(0, tab)), line.substring(tab + 1)) + } + } + new ScaffoldedPagesGenerator().generate(plan, new File(args[2]), args[3], origins) + } + + /** + * Writes, under {@code outputDir} where the resolver looks for it, the page for each domain + * class and each template in the directories planned for it. A template that cannot be + * expanded for a domain class is reported and left out. + * + * <p>A page ends with a comment naming the template it was expanded from, which renders as + * nothing, so that a page the build reports can be traced to the template to fix.</p> + * + * @param plan each domain class, with the templates directories to expand for it + * @param origins where the template in each templates directory came from; the template's own + * file for a directory not named + * @return how many pages were written + */ + int generate(Map<String, List<File>> plan, File outputDir, String encoding = 'UTF-8', Map<File, String> origins = [:]) { + int written = 0 + plan.each { String domain, List<File> templateDirs -> + Map<String, Object> model = model(domain).asMap() + for (Template template : read(templateDirs, origins)) { + String page + try { + page = ScaffoldedPages.expand(template.content, model) + } + catch (Exception e) { Review Comment: Agreed, and done without a flag. The generator now reports what it could not expand instead of printing it, and `generateScaffoldedViews` decides by whose copy it is: a dependency's is a warning, the application's own fails the task, naming each template and domain class (7d53c116c7). With your `<%-- probe --%>` appended to the example's `summary.gsp`, the build now stops with: ``` Could not expand the scaffolding template summary, from the application's summary.gsp, for com.example.Book: groovy.lang.MissingPropertyException: No such property: probe for class: groovy.lang.Binding ``` and the same line for each other domain class. On the compile side, `generatedDirectories` is replaced by an explicit list of optional pages (82027061d1). The generator reports every page it writes, `generateScaffoldedViews` lists only those expanded from a dependency's copy, and `compileGroovyPages` passes the list to the forked compiler as `grails.views.gsp.optionalPages`. A page from the application's template that expands but does not compile (I tried `\${ 1 + }` in `summary.gsp`) now fails `compileGroovyPages`, as a hand-written view does. ########## grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/views/gsp/GroovyPagePlugin.groovy: ########## @@ -397,9 +409,17 @@ class GroovyPagePlugin implements Plugin<Project> { it.compileStaticStrict.set(false) if (scaffolds) { it.dependsOn(tasks.named('stageGroovyPages')) + // a scaffolded page that does not compile is left to be produced when it is rendered, + // as it was before any was compiled, rather than failing the build + it.generatedDirectories.add(GenerateScaffoldedViewsTask.PAGES_DIRECTORY) Review Comment: Handled in d240490509, and for every page rather than only scaffolded ones. Before writing the new registry, the compiler reads the previous `views.properties`. Afterwards it removes the class, inner classes and data files of each page class the old registry named and the new one does not. It only removes what it registered itself, so there is no class-name prefix matching. Editing `summary.gsp` in the example and recompiling now leaves only the new digest's classes in `build/gsp-classes/main`. A renamed or removed hand-written view no longer lingers either. ########## grails-scaffolding/src/main/groovy/org/apache/grails/scaffolding/ScaffoldedPages.java: ########## @@ -0,0 +1,124 @@ +/* + * 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.apache.grails.scaffolding; + +import java.io.IOException; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; +import java.util.Map; +import java.util.TreeSet; + +import groovy.text.GStringTemplateEngine; + +/** + * Expands a scaffolding template into a page, and names the page the build compiles from it so + * that the runtime resolver can find it. + * + * <p>The build and the resolver both come here, so a page is expanded and named by the same code + * whichever of them does it. A page is named for the template's path, for readability, and for a + * digest of the template and of the value of each name of the model the template mentions. A + * template the build did not see, or saw expanded with a different model, so names a page that + * does not exist rather than a different one. A name the template never mentions is left out, so + * that a value that differs between the machine that built the application and the one running it + * - {@code packagePath} follows the file separator - does not cost the page; a template that reads + * the model without naming what it reads, through {@code binding} for instance, is named as though + * it did not read it.</p> + * + * @since 8.0 + */ +public final class ScaffoldedPages { + + /** + * The directory under the views root that holds the compiled pages. A controller's views are + * resolved from a directory named after a Java identifier, which cannot contain a hyphen, so no + * request for a controller's view reaches a page here. + */ + public static final String DIRECTORY = "grails-scaffolded"; + + /** Bytes of the digest kept in a page's name. */ + private static final int KEY_BYTES = 16; Review Comment: Done in 4a8cb1ac7e: 8 bytes, 16 hex characters. -- 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]
