jdaugherty commented on code in PR #15289: URL: https://github.com/apache/grails-core/pull/15289#discussion_r2619582531
########## grails-bootstrap/src/main/groovy/grails/codegen/model/DomainFieldModifier.groovy: ########## @@ -0,0 +1,347 @@ +/* + * 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 grails.codegen.model + +import groovy.transform.CompileStatic +import org.codehaus.groovy.ast.ClassNode +import org.codehaus.groovy.ast.FieldNode +import org.codehaus.groovy.ast.ModuleNode +import org.codehaus.groovy.ast.PropertyNode +import org.codehaus.groovy.control.CompilationUnit +import org.codehaus.groovy.control.CompilerConfiguration +import org.codehaus.groovy.control.Phases + +import java.nio.charset.StandardCharsets +import java.nio.file.Files + +/** + * Utility class for modifying domain class source files to add fields. + * + * @since 7.0 + */ +@CompileStatic +class DomainFieldModifier { + + /** + * Finds the domain class file for the given class name. + * + * @param projectDir the project root directory + * @param className the simple class name or fully qualified class name + * @return the domain class file, or null if not found + */ + File findDomainFile(File projectDir, String className) { + File domainDir = new File(projectDir, 'grails-app/domain') + if (!domainDir.exists()) { + return null + } + + String fileName = className.replace('.', '/') + '.groovy' + File exactMatch = new File(domainDir, fileName) + if (exactMatch.exists()) { + return exactMatch + } + + String simpleClassName = className.contains('.') ? className.substring(className.lastIndexOf('.') + 1) : className + File found = null + + domainDir.eachFileRecurse { File file -> + if (file.name == "${simpleClassName}.groovy") { + found = file + } + } + + found + } + + /** + * Checks if a field with the given name already exists in the domain class. + * + * @param domainFile the domain class file + * @param fieldName the field name to check + * @return true if the field exists, false otherwise + */ + boolean fieldExists(File domainFile, String fieldName) { + if (!domainFile?.exists()) { + return false + } + + try { + ClassNode classNode = parseClass(domainFile) + if (classNode == null) { + return false + } + + for (PropertyNode prop : classNode.properties) { + if (prop.name == fieldName) { + return true + } + } + + for (FieldNode field : classNode.fields) { + if (field.name == fieldName && !field.name.startsWith('$') && !field.name.startsWith('__')) { + return true + } + } + + return false + } catch (Exception e) { + return domainFile.text.contains("${fieldName}") Review Comment: If it refuses to parse, shouldn't we error? What's the scenario here? ########## grails-bootstrap/src/main/groovy/grails/codegen/model/FieldDefinition.groovy: ########## @@ -0,0 +1,268 @@ +/* + * 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 grails.codegen.model + +import groovy.transform.CompileStatic + +/** + * Represents a field definition for domain class code generation. + * + * @since 7.0 + */ +@CompileStatic +class FieldDefinition { + + /** + * Constraint style options for field validation. + */ + enum ConstraintStyle { + /** Use Grails static constraints block */ + GRAILS, + /** Use Jakarta Validation annotations */ + JAKARTA, + /** Use both Grails constraints and Jakarta annotations */ + BOTH + } + + static final Set<String> SUPPORTED_TYPES = [ Review Comment: Why only the built in types? Seems like enum would be a reasonable one ... ########## grails-bootstrap/src/main/groovy/grails/codegen/model/DomainFieldModifier.groovy: ########## @@ -0,0 +1,347 @@ +/* + * 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 grails.codegen.model + +import groovy.transform.CompileStatic +import org.codehaus.groovy.ast.ClassNode +import org.codehaus.groovy.ast.FieldNode +import org.codehaus.groovy.ast.ModuleNode +import org.codehaus.groovy.ast.PropertyNode +import org.codehaus.groovy.control.CompilationUnit +import org.codehaus.groovy.control.CompilerConfiguration +import org.codehaus.groovy.control.Phases + +import java.nio.charset.StandardCharsets +import java.nio.file.Files + +/** + * Utility class for modifying domain class source files to add fields. + * + * @since 7.0 + */ +@CompileStatic +class DomainFieldModifier { + + /** + * Finds the domain class file for the given class name. + * + * @param projectDir the project root directory + * @param className the simple class name or fully qualified class name + * @return the domain class file, or null if not found + */ + File findDomainFile(File projectDir, String className) { + File domainDir = new File(projectDir, 'grails-app/domain') + if (!domainDir.exists()) { + return null + } + + String fileName = className.replace('.', '/') + '.groovy' + File exactMatch = new File(domainDir, fileName) + if (exactMatch.exists()) { + return exactMatch + } + + String simpleClassName = className.contains('.') ? className.substring(className.lastIndexOf('.') + 1) : className + File found = null + + domainDir.eachFileRecurse { File file -> + if (file.name == "${simpleClassName}.groovy") { + found = file Review Comment: what if there are duplicate class names across different packages? I think you had an example where you had an Admin & User view - couldn't multiple domains exist in that case with a different table name configuration? ########## grails-bootstrap/src/main/groovy/grails/codegen/model/DomainFieldModifier.groovy: ########## @@ -0,0 +1,347 @@ +/* + * 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 grails.codegen.model + +import groovy.transform.CompileStatic +import org.codehaus.groovy.ast.ClassNode +import org.codehaus.groovy.ast.FieldNode +import org.codehaus.groovy.ast.ModuleNode +import org.codehaus.groovy.ast.PropertyNode +import org.codehaus.groovy.control.CompilationUnit +import org.codehaus.groovy.control.CompilerConfiguration +import org.codehaus.groovy.control.Phases + +import java.nio.charset.StandardCharsets +import java.nio.file.Files + +/** + * Utility class for modifying domain class source files to add fields. + * + * @since 7.0 + */ +@CompileStatic +class DomainFieldModifier { + + /** + * Finds the domain class file for the given class name. + * + * @param projectDir the project root directory + * @param className the simple class name or fully qualified class name + * @return the domain class file, or null if not found + */ + File findDomainFile(File projectDir, String className) { + File domainDir = new File(projectDir, 'grails-app/domain') + if (!domainDir.exists()) { + return null + } + + String fileName = className.replace('.', '/') + '.groovy' + File exactMatch = new File(domainDir, fileName) + if (exactMatch.exists()) { + return exactMatch + } + + String simpleClassName = className.contains('.') ? className.substring(className.lastIndexOf('.') + 1) : className + File found = null + + domainDir.eachFileRecurse { File file -> + if (file.name == "${simpleClassName}.groovy") { + found = file + } + } + + found + } + + /** + * Checks if a field with the given name already exists in the domain class. + * + * @param domainFile the domain class file + * @param fieldName the field name to check + * @return true if the field exists, false otherwise + */ + boolean fieldExists(File domainFile, String fieldName) { + if (!domainFile?.exists()) { + return false + } + + try { + ClassNode classNode = parseClass(domainFile) + if (classNode == null) { + return false + } + + for (PropertyNode prop : classNode.properties) { + if (prop.name == fieldName) { + return true + } + } + + for (FieldNode field : classNode.fields) { + if (field.name == fieldName && !field.name.startsWith('$') && !field.name.startsWith('__')) { Review Comment: I know Grails uses these prefix conventions for fields that should not be found, but can we at least extract these prefixes into a variable? Can you check if such a variable already exists so we don't forget to change this file over time? ########## grails-bootstrap/src/main/groovy/grails/codegen/model/DomainFieldModifier.groovy: ########## @@ -0,0 +1,347 @@ +/* + * 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 grails.codegen.model + +import groovy.transform.CompileStatic +import org.codehaus.groovy.ast.ClassNode +import org.codehaus.groovy.ast.FieldNode +import org.codehaus.groovy.ast.ModuleNode +import org.codehaus.groovy.ast.PropertyNode +import org.codehaus.groovy.control.CompilationUnit +import org.codehaus.groovy.control.CompilerConfiguration +import org.codehaus.groovy.control.Phases + +import java.nio.charset.StandardCharsets +import java.nio.file.Files + +/** + * Utility class for modifying domain class source files to add fields. + * + * @since 7.0 + */ +@CompileStatic +class DomainFieldModifier { + + /** + * Finds the domain class file for the given class name. + * + * @param projectDir the project root directory + * @param className the simple class name or fully qualified class name + * @return the domain class file, or null if not found + */ + File findDomainFile(File projectDir, String className) { + File domainDir = new File(projectDir, 'grails-app/domain') + if (!domainDir.exists()) { + return null + } + + String fileName = className.replace('.', '/') + '.groovy' + File exactMatch = new File(domainDir, fileName) + if (exactMatch.exists()) { + return exactMatch + } + + String simpleClassName = className.contains('.') ? className.substring(className.lastIndexOf('.') + 1) : className + File found = null + + domainDir.eachFileRecurse { File file -> + if (file.name == "${simpleClassName}.groovy") { + found = file + } + } + + found + } + + /** + * Checks if a field with the given name already exists in the domain class. + * + * @param domainFile the domain class file + * @param fieldName the field name to check + * @return true if the field exists, false otherwise + */ + boolean fieldExists(File domainFile, String fieldName) { + if (!domainFile?.exists()) { + return false + } + + try { + ClassNode classNode = parseClass(domainFile) + if (classNode == null) { + return false + } + + for (PropertyNode prop : classNode.properties) { + if (prop.name == fieldName) { + return true + } + } + + for (FieldNode field : classNode.fields) { + if (field.name == fieldName && !field.name.startsWith('$') && !field.name.startsWith('__')) { + return true + } + } + + return false + } catch (Exception e) { + return domainFile.text.contains("${fieldName}") + } + } + + /** + * Adds a field to the domain class file. + * + * @param domainFile the domain class file + * @param field the field definition to add + * @throws IllegalStateException if the file cannot be modified + */ + void addField(File domainFile, FieldDefinition field) { + if (!domainFile?.exists()) { + throw new IllegalStateException("Domain file does not exist: ${domainFile}") + } + + field.validate() + + List<String> lines = Files.readAllLines(domainFile.toPath(), StandardCharsets.UTF_8) + InsertionPoints points = findInsertionPoints(lines) + + int linesAdded = 0 + + if (field.usesJakartaAnnotations()) { + Set<String> requiredImports = field.getRequiredImports() + Set<String> existingImports = findExistingImports(lines) + + int importsAddedCount = 0 + for (String importClass : requiredImports) { + if (!existingImports.contains(importClass)) { + String importLine = 'import ' + importClass + lines.add(points.importInsertLine + linesAdded, importLine) + linesAdded++ + importsAddedCount++ + } + } + + if (importsAddedCount > 0) { + int lineAfterImports = points.importInsertLine + linesAdded + if (lineAfterImports < lines.size()) { + String nextLine = lines.get(lineAfterImports).trim() + if (!nextLine.isEmpty() && !nextLine.startsWith('import ')) { + lines.add(lineAfterImports, '') + linesAdded++ + } + } + } + } + + int fieldInsertIndex = points.fieldInsertLine + linesAdded + + if (field.usesJakartaAnnotations()) { + List<String> annotations = field.toAnnotations() + for (String annotation : annotations) { + lines.add(fieldInsertIndex, ' ' + annotation) + fieldInsertIndex++ + linesAdded++ + } + } + + String fieldDeclaration = " ${field.toFieldDeclaration()}" + lines.add(fieldInsertIndex, fieldDeclaration) + linesAdded++ + + if (field.usesGrailsConstraints()) { + String constraintLine = field.toConstraintLine() + if (constraintLine) { + if (points.hasConstraintsBlock) { + int constraintInsertIndex = points.constraintInsertLine + linesAdded + lines.add(constraintInsertIndex, ' ' + constraintLine) + } else { + int constraintBlockIndex = fieldInsertIndex + 1 + lines.add(constraintBlockIndex, '') + lines.add(constraintBlockIndex + 1, ' static constraints = {') + lines.add(constraintBlockIndex + 2, ' ' + constraintLine) + lines.add(constraintBlockIndex + 3, ' }') + } + } + } + + Files.write(domainFile.toPath(), lines, StandardCharsets.UTF_8) + } + + /** + * Finds existing import statements in the file. + */ + private Set<String> findExistingImports(List<String> lines) { + Set<String> imports = [] as Set + for (String line : lines) { + String trimmed = line.trim() + if (trimmed.startsWith('import ') && !trimmed.startsWith('import static')) { + String importClass = trimmed.substring(7).replace(';', '').trim() + imports.add(importClass) + } + } + imports + } + + /** + * Parses the Groovy source file and returns the main class node. + */ + private ClassNode parseClass(File sourceFile) { + CompilerConfiguration config = new CompilerConfiguration() Review Comment: What's the context of where this is supposed to be used? We usually used forked threads so not to continue expand the memory of the running process. ########## grails-forge/grails-forge-cli/build.gradle: ########## @@ -80,6 +80,8 @@ dependencies { annotationProcessor 'io.micronaut:micronaut-inject-java' implementation project(':grails-forge-core') + implementation "org.apache.grails.bootstrap:grails-bootstrap:$projectVersion" + implementation "org.codehaus.groovy:groovy:$groovyVersion" Review Comment: this is in the forge-cli, which is still a micronaut app and on groovy 3. ########## grails-bootstrap/src/main/groovy/grails/codegen/model/DomainFieldModifier.groovy: ########## @@ -0,0 +1,347 @@ +/* + * 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 grails.codegen.model + +import groovy.transform.CompileStatic +import org.codehaus.groovy.ast.ClassNode +import org.codehaus.groovy.ast.FieldNode +import org.codehaus.groovy.ast.ModuleNode +import org.codehaus.groovy.ast.PropertyNode +import org.codehaus.groovy.control.CompilationUnit +import org.codehaus.groovy.control.CompilerConfiguration +import org.codehaus.groovy.control.Phases + +import java.nio.charset.StandardCharsets +import java.nio.file.Files + +/** + * Utility class for modifying domain class source files to add fields. + * + * @since 7.0 + */ +@CompileStatic +class DomainFieldModifier { + + /** + * Finds the domain class file for the given class name. + * + * @param projectDir the project root directory + * @param className the simple class name or fully qualified class name + * @return the domain class file, or null if not found + */ + File findDomainFile(File projectDir, String className) { + File domainDir = new File(projectDir, 'grails-app/domain') + if (!domainDir.exists()) { + return null + } + + String fileName = className.replace('.', '/') + '.groovy' + File exactMatch = new File(domainDir, fileName) + if (exactMatch.exists()) { + return exactMatch + } + + String simpleClassName = className.contains('.') ? className.substring(className.lastIndexOf('.') + 1) : className + File found = null + + domainDir.eachFileRecurse { File file -> + if (file.name == "${simpleClassName}.groovy") { + found = file + } + } + + found + } + + /** + * Checks if a field with the given name already exists in the domain class. + * + * @param domainFile the domain class file + * @param fieldName the field name to check + * @return true if the field exists, false otherwise + */ + boolean fieldExists(File domainFile, String fieldName) { + if (!domainFile?.exists()) { + return false + } + + try { + ClassNode classNode = parseClass(domainFile) + if (classNode == null) { + return false + } + + for (PropertyNode prop : classNode.properties) { + if (prop.name == fieldName) { + return true + } + } + + for (FieldNode field : classNode.fields) { + if (field.name == fieldName && !field.name.startsWith('$') && !field.name.startsWith('__')) { + return true + } + } + + return false + } catch (Exception e) { + return domainFile.text.contains("${fieldName}") + } + } + + /** + * Adds a field to the domain class file. + * + * @param domainFile the domain class file + * @param field the field definition to add + * @throws IllegalStateException if the file cannot be modified + */ + void addField(File domainFile, FieldDefinition field) { + if (!domainFile?.exists()) { + throw new IllegalStateException("Domain file does not exist: ${domainFile}") + } + + field.validate() + + List<String> lines = Files.readAllLines(domainFile.toPath(), StandardCharsets.UTF_8) + InsertionPoints points = findInsertionPoints(lines) + + int linesAdded = 0 + + if (field.usesJakartaAnnotations()) { + Set<String> requiredImports = field.getRequiredImports() + Set<String> existingImports = findExistingImports(lines) + + int importsAddedCount = 0 + for (String importClass : requiredImports) { + if (!existingImports.contains(importClass)) { + String importLine = 'import ' + importClass + lines.add(points.importInsertLine + linesAdded, importLine) + linesAdded++ + importsAddedCount++ + } + } + + if (importsAddedCount > 0) { + int lineAfterImports = points.importInsertLine + linesAdded + if (lineAfterImports < lines.size()) { + String nextLine = lines.get(lineAfterImports).trim() + if (!nextLine.isEmpty() && !nextLine.startsWith('import ')) { + lines.add(lineAfterImports, '') + linesAdded++ + } + } + } + } + + int fieldInsertIndex = points.fieldInsertLine + linesAdded + + if (field.usesJakartaAnnotations()) { + List<String> annotations = field.toAnnotations() + for (String annotation : annotations) { + lines.add(fieldInsertIndex, ' ' + annotation) + fieldInsertIndex++ + linesAdded++ + } + } + + String fieldDeclaration = " ${field.toFieldDeclaration()}" + lines.add(fieldInsertIndex, fieldDeclaration) + linesAdded++ + + if (field.usesGrailsConstraints()) { + String constraintLine = field.toConstraintLine() + if (constraintLine) { + if (points.hasConstraintsBlock) { + int constraintInsertIndex = points.constraintInsertLine + linesAdded + lines.add(constraintInsertIndex, ' ' + constraintLine) + } else { + int constraintBlockIndex = fieldInsertIndex + 1 + lines.add(constraintBlockIndex, '') + lines.add(constraintBlockIndex + 1, ' static constraints = {') Review Comment: Shouldn't formatting be handled independently of line addition? ########## grails-bootstrap/src/main/groovy/grails/codegen/model/DomainFieldModifier.groovy: ########## @@ -0,0 +1,347 @@ +/* + * 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 grails.codegen.model + +import groovy.transform.CompileStatic +import org.codehaus.groovy.ast.ClassNode +import org.codehaus.groovy.ast.FieldNode +import org.codehaus.groovy.ast.ModuleNode +import org.codehaus.groovy.ast.PropertyNode +import org.codehaus.groovy.control.CompilationUnit +import org.codehaus.groovy.control.CompilerConfiguration +import org.codehaus.groovy.control.Phases + +import java.nio.charset.StandardCharsets +import java.nio.file.Files + +/** + * Utility class for modifying domain class source files to add fields. + * + * @since 7.0 + */ +@CompileStatic +class DomainFieldModifier { Review Comment: Can you help me understand why this is in bootstrap and not the CLI itself? Current files in this project all relate to classes that need shared across grails-core, gradle, and gorm. I only see you using this in the CLI libraries, why not put it in one of those or add a new project that's shared between the CLI for common dependencies? -- 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]
