diff --git a/docs/guide/src/docs/asciidoc/index.adoc b/docs/guide/src/docs/asciidoc/index.adoc index fd1a2020..7f4b34c0 100644 --- a/docs/guide/src/docs/asciidoc/index.adoc +++ b/docs/guide/src/docs/asciidoc/index.adoc @@ -153,6 +153,8 @@ include::{root-dir}/examples/micronaut-grails-example/src/test/groovy/micronaut/ There is an experimental generator for the JPA entities from GORM domain classes. You can use either from integration test or using Grails Console plugin +TIP: You can use `MicronautJdbcGenrator` instead of `MicronautJpaGenerator` to generate JDBC repositories instead of generic ones. + [source,groovy,indent=0,options="nowrap"] .Integration Test Usage ---- diff --git a/examples/micronaut-grails-example/grails-app/domain/micronaut/grails/example/Vehicle.groovy b/examples/micronaut-grails-example/grails-app/domain/micronaut/grails/example/Vehicle.groovy new file mode 100644 index 00000000..a6fbbba5 --- /dev/null +++ b/examples/micronaut-grails-example/grails-app/domain/micronaut/grails/example/Vehicle.groovy @@ -0,0 +1,33 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2020 Vladimir Orany. + * + * Licensed 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 micronaut.grails.example + +class Vehicle { + + String name + + String make + String model + + static constraints = { + name maxSize: 255 + make inList: ['Ford', 'Chevrolet', 'Nissan'] + model nullable: true + } + +} diff --git a/examples/micronaut-grails-example/src/test/groovy/micronaut/grails/example/GeneratorSpec.groovy b/examples/micronaut-grails-example/src/test/groovy/micronaut/grails/example/GeneratorSpec.groovy index 3d2a19df..6a710b57 100644 --- a/examples/micronaut-grails-example/src/test/groovy/micronaut/grails/example/GeneratorSpec.groovy +++ b/examples/micronaut-grails-example/src/test/groovy/micronaut/grails/example/GeneratorSpec.groovy @@ -35,12 +35,10 @@ class GeneratorSpec extends Specification { given: File root = initRootDirectory() when: - int generated = generator.generate(root) + generator.generate(root) then: noExceptionThrown() - generated == 1 - when: File entityFile = new File(root, 'micronaut/grails/example/User.groovy') File repositoryFile = new File(root, 'micronaut/grails/example/UserRepository.groovy') diff --git a/examples/micronaut-grails-example/src/test/groovy/micronaut/grails/example/MicronautJdbcGeneratorSpec.groovy b/examples/micronaut-grails-example/src/test/groovy/micronaut/grails/example/MicronautJdbcGeneratorSpec.groovy new file mode 100644 index 00000000..ea2182bf --- /dev/null +++ b/examples/micronaut-grails-example/src/test/groovy/micronaut/grails/example/MicronautJdbcGeneratorSpec.groovy @@ -0,0 +1,66 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2020 Vladimir Orany. + * + * Licensed 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 micronaut.grails.example + +// tag::body[] +import com.agorapulse.micronaut.grails.jpa.generator.MicronautJdbcGenerator +import com.agorapulse.micronaut.grails.test.MicronautGrailsIntegration +import com.agorapulse.testing.fixt.Fixt +import org.springframework.beans.factory.annotation.Autowired +import spock.lang.Specification + +@MicronautGrailsIntegration +class MicronautJdbcGeneratorSpec extends Specification { + + Fixt fixt = Fixt.create(MicronautJdbcGeneratorSpec) + + @Autowired MicronautJdbcGenerator generator + + void 'generate domains'() { + given: + File root = initRootDirectory() + when: + generator.generate(root) + then: + noExceptionThrown() + + when: + File entityFile = new File(root, 'micronaut/grails/example/Vehicle.groovy') + File repositoryFile = new File(root, 'micronaut/grails/example/VehicleRepository.groovy') + then: + entityFile.exists() + entityFile.text.trim() == fixt.readText('Vehicle.groovy.txt').trim() + + repositoryFile.exists() + repositoryFile.text.trim() == fixt.readText('VehicleRepository.groovy.txt').trim() + } + + private static File initRootDirectory() { + File root = new File(System.getProperty('java.io.tmpdir'), 'micronaut-data-model') + + if (root.exists()) { + root.deleteDir() + } + + root.mkdirs() + + return root + } + +} +// end::body[] diff --git a/examples/micronaut-grails-example/src/test/resources/micronaut/grails/example/MicronautJdbcGeneratorSpec/Vehicle.groovy.txt b/examples/micronaut-grails-example/src/test/resources/micronaut/grails/example/MicronautJdbcGeneratorSpec/Vehicle.groovy.txt new file mode 100644 index 00000000..828d3cbd --- /dev/null +++ b/examples/micronaut-grails-example/src/test/resources/micronaut/grails/example/MicronautJdbcGeneratorSpec/Vehicle.groovy.txt @@ -0,0 +1,37 @@ +package micronaut.grails.example + +import groovy.transform.CompileStatic +import javax.annotation.Nullable +import javax.persistence.Entity +import javax.persistence.GeneratedValue +import javax.persistence.GenerationType +import javax.persistence.Id +import javax.persistence.Version +import javax.validation.constraints.NotNull +import javax.validation.constraints.Pattern +import javax.validation.constraints.Size + +@Entity +@CompileStatic +class Vehicle { + + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + Long id + + @Version + Long version + + @NotNull + @Pattern(regexp = 'Ford|Chevrolet|Nissan') + @Size(max = 255) + String make + + @Nullable + @Size(max = 255) + String model + + @NotNull + @Size(max = 255) + String name + +} diff --git a/examples/micronaut-grails-example/src/test/resources/micronaut/grails/example/MicronautJdbcGeneratorSpec/VehicleRepository.groovy.txt b/examples/micronaut-grails-example/src/test/resources/micronaut/grails/example/MicronautJdbcGeneratorSpec/VehicleRepository.groovy.txt new file mode 100644 index 00000000..c15bcaf4 --- /dev/null +++ b/examples/micronaut-grails-example/src/test/resources/micronaut/grails/example/MicronautJdbcGeneratorSpec/VehicleRepository.groovy.txt @@ -0,0 +1,10 @@ +package micronaut.grails.example + +import io.micronaut.data.jdbc.annotation.JdbcRepository +import io.micronaut.data.model.query.builder.sql.Dialect +import io.micronaut.data.repository.CrudRepository + +@JdbcRepository(dialect = Dialect.MYSQL) +interface VehicleRepository extends CrudRepository { + +} diff --git a/subprojects/micronaut-grails-jpa-generator/src/main/groovy/com/agorapulse/micronaut/grails/jpa/generator/MicronautDataGenerator.groovy b/subprojects/micronaut-grails-jpa-generator/src/main/groovy/com/agorapulse/micronaut/grails/jpa/generator/MicronautDataGenerator.groovy new file mode 100644 index 00000000..1b60cb5a --- /dev/null +++ b/subprojects/micronaut-grails-jpa-generator/src/main/groovy/com/agorapulse/micronaut/grails/jpa/generator/MicronautDataGenerator.groovy @@ -0,0 +1,534 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2020 Vladimir Orany. + * + * Licensed 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 com.agorapulse.micronaut.grails.jpa.generator + +import grails.gorm.validation.ConstrainedProperty +import groovy.transform.CompileStatic +import org.grails.datastore.gorm.validation.constraints.eval.ConstraintsEvaluator +import org.grails.datastore.mapping.config.Property +import org.grails.datastore.mapping.core.Datastore +import org.grails.datastore.mapping.model.PersistentEntity +import org.grails.datastore.mapping.model.PersistentProperty +import org.grails.datastore.mapping.model.types.ManyToOne +import org.grails.datastore.mapping.model.types.OneToMany +import org.grails.datastore.mapping.model.types.OneToOne +import org.grails.datastore.mapping.model.types.Simple + +import javax.persistence.FetchType +import javax.persistence.PostLoad +import javax.persistence.PostPersist +import javax.persistence.PostRemove +import javax.persistence.PostUpdate +import javax.persistence.PrePersist +import javax.persistence.PreRemove +import javax.persistence.PreUpdate + +/** + * Experimental generator of JPA entities based on GORM entities. + */ +@CompileStatic +@SuppressWarnings(['MethodSize', 'DuplicateStringLiteral']) +abstract class MicronautDataGenerator { + + private static final Map GORM_HOOKS_TO_ANNOTATIONS = [ + beforeInsert: PrePersist, + beforeUpdate: PreUpdate, + beforeDelete: PreRemove, + afterInsert : PostPersist, + afterUpdate : PostUpdate, + afterDelete : PostRemove, + onLoad : PostLoad, + ].asImmutable() as Map + + protected final Datastore datastore + protected final ConstraintsEvaluator constraintsEvaluator + + protected MicronautDataGenerator(Datastore datastore, ConstraintsEvaluator constraintsEvaluator) { + this.constraintsEvaluator = constraintsEvaluator + this.datastore = datastore + } + + @SuppressWarnings('NestedForLoop') + int generate(File root) { + Collection entities = datastore.mappingContext.persistentEntities + entities.each { PersistentEntity entity -> + File packageDirectory = new File(root, (entity.javaClass.package.name).replace('.', File.separator)) + packageDirectory.mkdirs() + + File entityFile = new File(packageDirectory, "${entity.javaClass.simpleName}.groovy") + entityFile.text = generateEntity(entity) + + File repositoryFile = new File(packageDirectory, "${entity.javaClass.simpleName}Repository.groovy") + repositoryFile.text = generateRepository(entity) + } + + List requiredEnums = [] + for (PersistentEntity entity in datastore.mappingContext.persistentEntities) { + for (PersistentProperty property in entity.persistentProperties) { + if (property.type.enum) { + requiredEnums.add(property.type) + } + } + } + + requiredEnums.each { Class enumType -> + copyEnum(root, enumType) + } + + return entities.size() + } + + @SuppressWarnings([ + 'AbcMetric', + 'UnnecessaryObjectReferences', + 'ImplicitClosureParameter', + 'Instanceof', + 'LineLength', + 'MethodSize', + 'UnnecessaryCollectCall', + ]) + String generateEntity(PersistentEntity entity) { + List unifiedProperties = collectUnifiedProperties(entity) + Set imports = new TreeSet<>(unifiedProperties.collect { + if (it.persistentProperty instanceof OneToMany) { + return (it.persistentProperty as OneToMany).associatedEntity.javaClass + } + return it.persistentProperty.type + }.findAll { + it && !it.primitive && it.package && it.package != entity.javaClass.package && it.package.name != 'java.util' && it.package.name != 'java.lang' + }.collect { + it.name + }) + + StringWriter bodyStringWriter = new StringWriter() + PrintWriter body = new PrintWriter(bodyStringWriter) + + imports.add('javax.persistence.Entity') + imports.add(CompileStatic.name) + + if (entity.javaClass.name != entity.name) { + body.println('@CompileStatic') + body.println("""@Entity(name="$entity.name")""") + } else { + body.println('@Entity') + body.println('@CompileStatic') + } + + List uniqueProperties = unifiedProperties.findAll { !it.index && it.uniqueColumnNames } + List indexedProperties = unifiedProperties.findAll { it.index } + + if (uniqueProperties || indexedProperties) { + imports.add('javax.persistence.Table') + body.println('@Table(') + if (indexedProperties) { + imports.add('javax.persistence.Index') + body.println(' indexes = [') + body.println(indexedProperties.collect { UnifiedProperty indexed -> + StringWriter indexAnnotation = new StringWriter() + indexAnnotation.print(' @Index(') + + if (indexed.index instanceof String) { + indexAnnotation.print("name = '$indexed.index', ") + } + if (indexed.uniqueColumnNames.size() == 1) { + indexAnnotation.print('unique = true, ') + } + indexAnnotation.print("columnList = '${indexed.columnName}')") + return indexAnnotation.toString() + }.join(',\n')) + if (uniqueProperties) { + body.println(' ],') + } else { + body.println(' ]') + } + } + if (uniqueProperties) { + imports.add('javax.persistence.UniqueConstraint') + body.println(' uniqueConstraints = [') + body.println(uniqueProperties.collect { it -> " @UniqueConstraint(columnNames = [${it.uniqueColumnNames.collect { column -> "'$column'" }.join(', ')}])" }.join(',\n')) + body.println(' ]') + } + body.println(')') + } + + body.println("class $entity.javaClass.simpleName {") + body.println() + + if (entity.identity) { + imports.add('javax.persistence.Id') + imports.add('javax.persistence.GeneratedValue') + imports.add('javax.persistence.GenerationType') + + body.println(' @Id @GeneratedValue(strategy = GenerationType.IDENTITY)') + body.println(" $entity.identity.type.simpleName $entity.identity.name") + body.println() + } + + if (entity.versioned) { + imports.add('javax.persistence.Version') + body.println(' @Version') + body.println(" $entity.version.type.simpleName $entity.version.name") + body.println() + } + + for (UnifiedProperty property in unifiedProperties) { + printProperty(property, body, imports) + } + + GORM_HOOKS_TO_ANNOTATIONS.each { String methodName, Class annotation -> + if (entity.javaClass.declaredMethods.any { it.name == methodName }) { + imports.add(annotation.name) + body.println(" @$annotation.simpleName") + body.println(" void $methodName() {") + body.println(' // TODO: reimplement') + body.println(' }') + body.println() + } + } + + body.println('}') + + StringWriter finalWriter = new StringWriter() + PrintWriter finalPrintWriter = new PrintWriter(finalWriter) + finalPrintWriter.println "package $entity.javaClass.package.name" + finalPrintWriter.println() + + imports.each { String imported -> + finalPrintWriter.println("import $imported") + } + + finalPrintWriter.println() + finalPrintWriter.println(bodyStringWriter.toString()) + + return finalWriter.toString() + } + + @SuppressWarnings('LineLength') + private static void printProperty( + UnifiedProperty unified, + PrintWriter writer, + Set imports + ) { + switch (unified.persistentProperty) { + case Simple: + printSimpleProperty(writer, unified, imports) + break + case OneToOne: + printOneToOne(writer, unified, imports) + break + case OneToMany: + printOneToMany(unified, writer, imports) + break + case ManyToOne: + printManyToOne(writer, unified, imports) + break + default: + writer.println(" // TODO: property type ${unified.persistentProperty.getClass()}. not yet handled") + writer.print(" $unified.persistentProperty.type.simpleName $unified.persistentProperty.name") + writer.println() + writer.println() + } + } + + private static void printManyToOne(PrintWriter writer, UnifiedProperty unified, Set imports) { + imports.add('javax.persistence.ManyToOne') + writer.print(' @ManyToOne') + if (unified.mappingProperty.lazy || unified.mappingProperty.fetchStrategy && unified.mappingProperty.fetchStrategy != FetchType.EAGER) { + imports.add('javax.persistence.FetchType') + writer.print('(fetch = FetchType.LAZY)') + } + writer.println() + writer.println(" $unified.persistentProperty.type.simpleName $unified.persistentProperty.name") + writer.println() + } + + @SuppressWarnings(['AbcMetric', 'LineLength']) + private static void printOneToMany(UnifiedProperty unified, PrintWriter writer, Set imports) { + OneToMany oneToMany = unified.persistentProperty as OneToMany + List parts = [] + + if (oneToMany.referencedPropertyName) { + parts.add("mappedBy = '$oneToMany.referencedPropertyName'") + } + + if (unified.mappingProperty.lazy == Boolean.FALSE || unified.mappingProperty.fetchStrategy && unified.mappingProperty.fetchStrategy != FetchType.LAZY) { + imports.add('javax.persistence.FetchType') + parts.add('fetch = FetchType.EAGER') + } + + if (unified.mappingProperty.cascade == 'all-delete-orphan') { + imports.add('javax.persistence.CascadeType') + parts.add('cascade = CascadeType.ALL, orphanRemoval = true') + } else if (unified.mappingProperty.cascade) { + imports.add('javax.persistence.CascadeType') + parts.add("cascade = CascadeType.${unified.mappingProperty.cascade.toUpperCase()}") + } else if (unified.mappingProperty.cascades) { + imports.add('javax.persistence.CascadeType') + parts.add("cascade = CascadeType.${unified.mappingProperty.cascades.first()}") + } + + imports.add('javax.persistence.OneToMany') + + if (parts) { + writer.println(" @OneToMany(${parts.join(', ')})") + } else { + writer.println(' @OneToMany') + } + + if (unified.joinTable && unified.joinTable.key && unified.joinTable.column) { + imports.add('javax.persistence.JoinTable') + imports.add('javax.persistence.JoinColumn') + writer.println(" @JoinTable(joinColumns = @JoinColumn(name = '$unified.joinTable.key.name'), inverseJoinColumns = @JoinColumn(name = '$unified.joinTable.column.name'))") + } + + if (unified.sort) { + imports.add('javax.persistence.OrderBy') + if (unified.order) { + writer.println(" @OrderBy('${unified.sort} ${unified.order.toUpperCase()}')") + } else { + writer.println(" @OrderBy('${unified.sort}')") + } + } + + writer.println(" ${unified.sort ? 'List' : 'Set'}<$oneToMany.associatedEntity.javaClass.simpleName> $unified.persistentProperty.name") + writer.println() + } + + private static void printOneToOne(PrintWriter writer, UnifiedProperty unified, Set imports) { + imports.add('javax.persistence.OneToOne') + writer.print(' @OneToOne') + if (unified.mappingProperty.lazy || unified.mappingProperty.fetchStrategy && unified.mappingProperty.fetchStrategy != FetchType.EAGER) { + imports.add('javax.persistence.FetchType') + writer.print('(fetch = FetchType.LAZY)') + } + writer.println() + writer.println(" $unified.persistentProperty.type.simpleName $unified.persistentProperty.name") + writer.println() + } + + @SuppressWarnings(['AbcMetric', 'CyclomaticComplexity', 'MethodSize']) + private static void printSimpleProperty( + PrintWriter writer, + UnifiedProperty unified, + Set imports + ) { + if (unified.nullable) { + imports.add('javax.annotation.Nullable') + writer.println(' @Nullable') + } else if (!unified.persistentProperty.type.primitive) { + imports.add('javax.validation.constraints.NotNull') + writer.println(' @NotNull') + } + + if (unified.range == null && unified.min != null) { + imports.add('javax.validation.constraints.Min') + writer.println(" @Min(${unified.min}L)") + } else if (unified.range == null && unified.min != null) { + imports.add('javax.validation.constraints.Min') + writer.println(" @Min(${unified.min}L)") + } + + if (unified.range == null && unified.max != null) { + imports.add('javax.validation.constraints.Max') + writer.println(" @Max(${unified.max}L)") + } else if (unified.range == null && unified.max != null) { + imports.add('javax.validation.constraints.Max') + writer.println(" @Max(${unified.max}L)") + } + + if (unified.range != null) { + imports.add('javax.validation.constraints.Max') + imports.add('javax.validation.constraints.Min') + writer.println(" @Min(${unified.range.from}L)") + writer.println(" @Max(${unified.range.to}L)") + } + + if (unified.size != null) { + imports.add('javax.validation.constraints.Size') + writer.println(" @Size(min = $unified.size.from, max = $unified.size.to)") + } + + if (unified.scale != null) { + imports.add('javax.validation.constraints.Digits') + writer.println(" @Digits(fraction = $unified.scale)") + } + + if (!unified.blank) { + imports.add('javax.validation.constraints.NotBlank') + writer.println(' @NotBlank') + } + + if (unified.persistentProperty.type == String && unified.email) { + imports.add('javax.validation.constraints.Email') + writer.println(' @Email') + } + + if (unified.persistentProperty.type == String && unified.matches != null) { + imports.add('javax.validation.constraints.Pattern') + writer.println(" @Pattern(regexp = '$unified.matches')") + } + + if (unified.inList != null) { + imports.add('javax.validation.constraints.Pattern') + writer.println(" @Pattern(regexp = '${unified.inList.join('|')}')") + } + + if (unified.persistentProperty.type == String && unified.creditCard) { + writer.println(' // TODO: must be credit card') + } + + if (unified.notEqual) { + writer.println(" // TODO: must not be equal to '$unified.notEqual'") + } + + if (unified.minSize != null && unified.maxSize != null) { + imports.add('javax.validation.constraints.Size') + writer.println(" @Size(min = $unified.minSize, max = $unified.maxSize)") + } else if (unified.maxSize != null) { + imports.add('javax.validation.constraints.Size') + writer.println(" @Size(max = $unified.maxSize)") + } else if (unified.minSize != null) { + imports.add('javax.validation.constraints.Size') + writer.println(" @Size(min = $unified.minSize)") + } + + if (unified.enumType) { + imports.add('javax.persistence.Enumerated') + imports.add('javax.persistence.EnumType') + writer.println(" @Enumerated(EnumType.$unified.enumType)") + } + + if (unified.sqlType || unified.sqlColumnName) { + imports.add('javax.persistence.Column') + writer.print(' @Column(') + if (unified.sqlType) { + writer.print("columnDefinition = '${unified.sqlType}'") + if (unified.sqlColumnName) { + writer.print(', ') + } + } + if (unified.sqlColumnName) { + writer.print("name = '${unified.sqlColumnName}'") + } + + writer.println(')') + } + + if (unified.persistentProperty.name == 'dateCreated') { + imports.add('io.micronaut.data.annotation.DateCreated') + writer.println(' @DateCreated') + } + + if (unified.persistentProperty.name == 'lastUpdated') { + imports.add('io.micronaut.data.annotation.DateUpdated') + writer.println(' @DateUpdated') + } + + writer.print(" $unified.persistentProperty.type.simpleName $unified.persistentProperty.name") + + Object defaultValue = unified.defaultValue + if (defaultValue != null && defaultValue != 0 && defaultValue != Boolean.FALSE) { + // further - column definition? + switch (defaultValue) { + case CharSequence: + writer.print " = '${defaultValue}'" + break + case Number: + case Boolean: + writer.print " = ${defaultValue}" + break + case Enum: + writer.print " = ${unified.persistentProperty.type.simpleName}.${defaultValue}" + break + case Date: + long delta = Math.abs(System.currentTimeMillis() - (defaultValue as Date).time) + if (delta < 1000) { + writer.print ' = new Date()' + } else { + writer.print " // TODO: defaults to '${defaultValue}'" + } + break + case Currency: + writer.print(" = Currency.getInstance('$defaultValue')") + break + case Locale.default: + writer.print(' = Locale.default') + break + case Locale.ENGLISH: + writer.print(' = Locale.ENGLISH') + break + case Locale: + Locale defaultLocale = defaultValue as Locale + writer.print(" = new Locale('${defaultLocale.language}', '${defaultLocale.country}')") + break + case TimeZone.default: + writer.print(' = TimeZone.default') + break + default: + writer.print " // TODO: defaults to '${defaultValue}'" + } + } + + writer.println() + writer.println() + } + + @SuppressWarnings('LineLength') + protected abstract String generateRepository(PersistentEntity entity) + + private static void copyEnum(File root, Class enumType) { + if (!enumType.enum) { + return + } + + File packageDirectory = new File(root, enumType.package.name.replace('.', File.separator)) + packageDirectory.mkdirs() + + File enumFile = new File(packageDirectory, "${enumType.simpleName}.java") + + List enumValues = enumType.getMethod('values').invoke(null) as List + + enumFile.text = """ + package $enumType.package.name; + + public enum $enumType.simpleName { + // TODO: migrate original enum + ${enumValues.join(', ')}; + } + """.stripIndent().trim() + } + + @SuppressWarnings('ImplicitClosureParameter') + private List collectUnifiedProperties(PersistentEntity entity) { + Object newInstance = entity.newInstance() + Map constraintedProperties = constraintsEvaluator.evaluate(entity.javaClass) + List unifiedProperties = entity + .persistentProperties + .sort(false) { it.name } + .findAll { entity.identity?.name != it.name && entity.version?.name != it.name } + .collect { PersistentProperty property -> + ConstrainedProperty constrainedProperty = constraintedProperties.get(property.name) + Property mappingProperty = property.mapping.mappedForm as Property + Object value = property.reader.read(newInstance) + + return new UnifiedProperty(property, constrainedProperty, mappingProperty, value) + } + return unifiedProperties + } + +} diff --git a/subprojects/micronaut-grails-jpa-generator/src/main/groovy/com/agorapulse/micronaut/grails/jpa/generator/MicronautJdbcGenerator.groovy b/subprojects/micronaut-grails-jpa-generator/src/main/groovy/com/agorapulse/micronaut/grails/jpa/generator/MicronautJdbcGenerator.groovy new file mode 100644 index 00000000..72e7e582 --- /dev/null +++ b/subprojects/micronaut-grails-jpa-generator/src/main/groovy/com/agorapulse/micronaut/grails/jpa/generator/MicronautJdbcGenerator.groovy @@ -0,0 +1,51 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2020 Vladimir Orany. + * + * Licensed 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 com.agorapulse.micronaut.grails.jpa.generator + +import groovy.transform.CompileStatic +import org.grails.datastore.gorm.validation.constraints.eval.ConstraintsEvaluator +import org.grails.datastore.mapping.core.Datastore +import org.grails.datastore.mapping.model.PersistentEntity + +/** + * Experimental generator of Micronaut DATA JDBC entities based on GORM entities. + */ +@CompileStatic +class MicronautJdbcGenerator extends MicronautDataGenerator { + + MicronautJdbcGenerator(Datastore datastore, ConstraintsEvaluator constraintsEvaluator) { + super(datastore, constraintsEvaluator) + } + + @Override + @SuppressWarnings('LineLength') + protected String generateRepository(PersistentEntity entity) { + return """ + package $entity.javaClass.package.name + + import io.micronaut.data.jdbc.annotation.JdbcRepository + import io.micronaut.data.model.query.builder.sql.Dialect + import io.micronaut.data.repository.CrudRepository + + @JdbcRepository(dialect = Dialect.MYSQL) + interface ${entity.javaClass.simpleName}Repository extends CrudRepository<${entity.javaClass.simpleName}, ${entity.identity.type.simpleName}> { + + } + """.stripIndent().trim() + } +} diff --git a/subprojects/micronaut-grails-jpa-generator/src/main/groovy/com/agorapulse/micronaut/grails/jpa/generator/MicronautJpaGenerator.groovy b/subprojects/micronaut-grails-jpa-generator/src/main/groovy/com/agorapulse/micronaut/grails/jpa/generator/MicronautJpaGenerator.groovy index 9268634b..8c6b5297 100644 --- a/subprojects/micronaut-grails-jpa-generator/src/main/groovy/com/agorapulse/micronaut/grails/jpa/generator/MicronautJpaGenerator.groovy +++ b/subprojects/micronaut-grails-jpa-generator/src/main/groovy/com/agorapulse/micronaut/grails/jpa/generator/MicronautJpaGenerator.groovy @@ -17,478 +17,24 @@ */ package com.agorapulse.micronaut.grails.jpa.generator -import grails.gorm.validation.ConstrainedProperty import groovy.transform.CompileStatic import org.grails.datastore.gorm.validation.constraints.eval.ConstraintsEvaluator -import org.grails.datastore.mapping.config.Property import org.grails.datastore.mapping.core.Datastore import org.grails.datastore.mapping.model.PersistentEntity -import org.grails.datastore.mapping.model.PersistentProperty -import org.grails.datastore.mapping.model.types.ManyToOne -import org.grails.datastore.mapping.model.types.OneToMany -import org.grails.datastore.mapping.model.types.OneToOne -import org.grails.datastore.mapping.model.types.Simple - -import javax.persistence.FetchType -import javax.persistence.PostLoad -import javax.persistence.PostPersist -import javax.persistence.PostRemove -import javax.persistence.PostUpdate -import javax.persistence.PrePersist -import javax.persistence.PreRemove -import javax.persistence.PreUpdate /** * Experimental generator of JPA entities based on GORM entities. */ @CompileStatic -@SuppressWarnings(['MethodSize', 'DuplicateStringLiteral']) -class MicronautJpaGenerator { - - private static final Map GORM_HOOKS_TO_ANNOTATIONS = [ - beforeInsert: PrePersist, - beforeUpdate: PreUpdate, - beforeDelete: PreRemove, - afterInsert : PostPersist, - afterUpdate : PostUpdate, - afterDelete : PostRemove, - onLoad : PostLoad, - ].asImmutable() as Map - - private final Datastore datastore - private final ConstraintsEvaluator constraintsEvaluator +class MicronautJpaGenerator extends MicronautDataGenerator { MicronautJpaGenerator(Datastore datastore, ConstraintsEvaluator constraintsEvaluator) { - this.constraintsEvaluator = constraintsEvaluator - this.datastore = datastore - } - - @SuppressWarnings('NestedForLoop') - int generate(File root) { - Collection entities = datastore.mappingContext.persistentEntities - entities.each { PersistentEntity entity -> - File packageDirectory = new File(root, (entity.javaClass.package.name).replace('.', File.separator)) - packageDirectory.mkdirs() - - File entityFile = new File(packageDirectory, "${entity.javaClass.simpleName}.groovy") - entityFile.text = generateEntity(entity) - - File repositoryFile = new File(packageDirectory, "${entity.javaClass.simpleName}Repository.groovy") - repositoryFile.text = generateRepository(entity) - } - - List requiredEnums = [] - for (PersistentEntity entity in datastore.mappingContext.persistentEntities) { - for (PersistentProperty property in entity.persistentProperties) { - if (property.type.enum) { - requiredEnums.add(property.type) - } - } - } - - requiredEnums.each { Class enumType -> - copyEnum(root, enumType) - } - - return entities.size() - } - - @SuppressWarnings([ - 'AbcMetric', - 'UnnecessaryObjectReferences', - 'ImplicitClosureParameter', - 'Instanceof', - 'LineLength', - 'MethodSize', - 'UnnecessaryCollectCall', - ]) - String generateEntity(PersistentEntity entity) { - List unifiedProperties = collectUnifiedProperties(entity) - Set imports = new TreeSet<>(unifiedProperties.collect { - if (it.persistentProperty instanceof OneToMany) { - return (it.persistentProperty as OneToMany).associatedEntity.javaClass - } - return it.persistentProperty.type - }.findAll { - it && !it.primitive && it.package && it.package != entity.javaClass.package && it.package.name != 'java.util' && it.package.name != 'java.lang' - }.collect { - it.name - }) - - StringWriter bodyStringWriter = new StringWriter() - PrintWriter body = new PrintWriter(bodyStringWriter) - - imports.add('javax.persistence.Entity') - imports.add(CompileStatic.name) - - if (entity.javaClass.name != entity.name) { - body.println('@CompileStatic') - body.println("""@Entity(name="$entity.name")""") - } else { - body.println('@Entity') - body.println('@CompileStatic') - } - - List uniqueProperties = unifiedProperties.findAll { !it.index && it.uniqueColumnNames } - List indexedProperties = unifiedProperties.findAll { it.index } - - if (uniqueProperties || indexedProperties) { - imports.add('javax.persistence.Table') - body.println('@Table(') - if (indexedProperties) { - imports.add('javax.persistence.Index') - body.println(' indexes = [') - body.println(indexedProperties.collect { UnifiedProperty indexed -> - StringWriter indexAnnotation = new StringWriter() - indexAnnotation.print(' @Index(') - - if (indexed.index instanceof String) { - indexAnnotation.print("name = '$indexed.index', ") - } - if (indexed.uniqueColumnNames.size() == 1) { - indexAnnotation.print('unique = true, ') - } - indexAnnotation.print("columnList = '${indexed.columnName}')") - return indexAnnotation.toString() - }.join(',\n')) - if (uniqueProperties) { - body.println(' ],') - } else { - body.println(' ]') - } - } - if (uniqueProperties) { - imports.add('javax.persistence.UniqueConstraint') - body.println(' uniqueConstraints = [') - body.println(uniqueProperties.collect { it -> " @UniqueConstraint(columnNames = [${it.uniqueColumnNames.collect { column -> "'$column'" }.join(', ')}])" }.join(',\n')) - body.println(' ]') - } - body.println(')') - } - - body.println("class $entity.javaClass.simpleName {") - body.println() - - if (entity.identity) { - imports.add('javax.persistence.Id') - imports.add('javax.persistence.GeneratedValue') - imports.add('javax.persistence.GenerationType') - - body.println(' @Id @GeneratedValue(strategy = GenerationType.IDENTITY)') - body.println(" $entity.identity.type.simpleName $entity.identity.name") - body.println() - } - - if (entity.versioned) { - imports.add('javax.persistence.Version') - body.println(' @Version') - body.println(" $entity.version.type.simpleName $entity.version.name") - body.println() - } - - for (UnifiedProperty property in unifiedProperties) { - printProperty(property, body, imports) - } - - GORM_HOOKS_TO_ANNOTATIONS.each { String methodName, Class annotation -> - if (entity.javaClass.declaredMethods.any { it.name == methodName }) { - imports.add(annotation.name) - body.println(" @$annotation.simpleName") - body.println(" void $methodName() {") - body.println(' // TODO: reimplement') - body.println(' }') - body.println() - } - } - - body.println('}') - - StringWriter finalWriter = new StringWriter() - PrintWriter finalPrintWriter = new PrintWriter(finalWriter) - finalPrintWriter.println "package $entity.javaClass.package.name" - finalPrintWriter.println() - - imports.each { String imported -> - finalPrintWriter.println("import $imported") - } - - finalPrintWriter.println() - finalPrintWriter.println(bodyStringWriter.toString()) - - return finalWriter.toString() - } - - @SuppressWarnings('LineLength') - private static void printProperty( - UnifiedProperty unified, - PrintWriter writer, - Set imports - ) { - switch (unified.persistentProperty) { - case Simple: - printSimpleProperty(writer, unified, imports) - break - case OneToOne: - printOneToOne(writer, unified, imports) - break - case OneToMany: - printOneToMany(unified, writer, imports) - break - case ManyToOne: - printManyToOne(writer, unified, imports) - break - default: - writer.println(" // TODO: property type ${unified.persistentProperty.getClass()}. not yet handled") - writer.print(" $unified.persistentProperty.type.simpleName $unified.persistentProperty.name") - writer.println() - writer.println() - } - } - - private static void printManyToOne(PrintWriter writer, UnifiedProperty unified, Set imports) { - imports.add('javax.persistence.ManyToOne') - writer.print(' @ManyToOne') - if (unified.mappingProperty.lazy || unified.mappingProperty.fetchStrategy && unified.mappingProperty.fetchStrategy != FetchType.EAGER) { - imports.add('javax.persistence.FetchType') - writer.print('(fetch = FetchType.LAZY)') - } - writer.println() - writer.println(" $unified.persistentProperty.type.simpleName $unified.persistentProperty.name") - writer.println() - } - - @SuppressWarnings(['AbcMetric', 'LineLength']) - private static void printOneToMany(UnifiedProperty unified, PrintWriter writer, Set imports) { - OneToMany oneToMany = unified.persistentProperty as OneToMany - List parts = [] - - if (oneToMany.referencedPropertyName) { - parts.add("mappedBy = '$oneToMany.referencedPropertyName'") - } - - if (unified.mappingProperty.lazy == Boolean.FALSE || unified.mappingProperty.fetchStrategy && unified.mappingProperty.fetchStrategy != FetchType.LAZY) { - imports.add('javax.persistence.FetchType') - parts.add('fetch = FetchType.EAGER') - } - - if (unified.mappingProperty.cascade == 'all-delete-orphan') { - imports.add('javax.persistence.CascadeType') - parts.add('cascade = CascadeType.ALL, orphanRemoval = true') - } else if (unified.mappingProperty.cascade) { - imports.add('javax.persistence.CascadeType') - parts.add("cascade = CascadeType.${unified.mappingProperty.cascade.toUpperCase()}") - } else if (unified.mappingProperty.cascades) { - imports.add('javax.persistence.CascadeType') - parts.add("cascade = CascadeType.${unified.mappingProperty.cascades.first()}") - } - - imports.add('javax.persistence.OneToMany') - - if (parts) { - writer.println(" @OneToMany(${parts.join(', ')})") - } else { - writer.println(' @OneToMany') - } - - if (unified.joinTable && unified.joinTable.key && unified.joinTable.column) { - imports.add('javax.persistence.JoinTable') - imports.add('javax.persistence.JoinColumn') - writer.println(" @JoinTable(joinColumns = @JoinColumn(name = '$unified.joinTable.key.name'), inverseJoinColumns = @JoinColumn(name = '$unified.joinTable.column.name'))") - } - - if (unified.sort) { - imports.add('javax.persistence.OrderBy') - if (unified.order) { - writer.println(" @OrderBy('${unified.sort} ${unified.order.toUpperCase()}')") - } else { - writer.println(" @OrderBy('${unified.sort}')") - } - } - - writer.println(" ${unified.sort ? 'List' : 'Set'}<$oneToMany.associatedEntity.javaClass.simpleName> $unified.persistentProperty.name") - writer.println() - } - - private static void printOneToOne(PrintWriter writer, UnifiedProperty unified, Set imports) { - imports.add('javax.persistence.OneToOne') - writer.print(' @OneToOne') - if (unified.mappingProperty.lazy || unified.mappingProperty.fetchStrategy && unified.mappingProperty.fetchStrategy != FetchType.EAGER) { - imports.add('javax.persistence.FetchType') - writer.print('(fetch = FetchType.LAZY)') - } - writer.println() - writer.println(" $unified.persistentProperty.type.simpleName $unified.persistentProperty.name") - writer.println() - } - - @SuppressWarnings(['AbcMetric', 'CyclomaticComplexity', 'MethodSize']) - private static void printSimpleProperty( - PrintWriter writer, - UnifiedProperty unified, - Set imports - ) { - if (unified.nullable) { - imports.add('javax.annotation.Nullable') - writer.println(' @Nullable') - } else if (!unified.persistentProperty.type.primitive) { - imports.add('javax.validation.constraints.NotNull') - writer.println(' @NotNull') - } - - if (unified.range == null && unified.min != null) { - imports.add('javax.validation.constraints.Min') - writer.println(" @Min(${unified.min}L)") - } else if (unified.range == null && unified.min != null) { - imports.add('javax.validation.constraints.Min') - writer.println(" @Min(${unified.min}L)") - } - - if (unified.range == null && unified.max != null) { - imports.add('javax.validation.constraints.Max') - writer.println(" @Max(${unified.max}L)") - } else if (unified.range == null && unified.max != null) { - imports.add('javax.validation.constraints.Max') - writer.println(" @Max(${unified.max}L)") - } - - if (unified.range != null) { - imports.add('javax.validation.constraints.Max') - imports.add('javax.validation.constraints.Min') - writer.println(" @Min(${unified.range.from}L)") - writer.println(" @Max(${unified.range.to}L)") - } - - if (unified.size != null) { - imports.add('javax.validation.constraints.Size') - writer.println(" @Size(min = $unified.size.from, max = $unified.size.to)") - } - - if (unified.scale != null) { - imports.add('javax.validation.constraints.Digits') - writer.println(" @Digits(fraction = $unified.scale)") - } - - if (!unified.blank) { - imports.add('javax.validation.constraints.NotBlank') - writer.println(' @NotBlank') - } - - if (unified.persistentProperty.type == String && unified.email) { - imports.add('javax.validation.constraints.Email') - writer.println(' @Email') - } - - if (unified.persistentProperty.type == String && unified.matches != null) { - imports.add('javax.validation.constraints.Pattern') - writer.println(" @Pattern(regexp = $unified.matches)") - } - - if (unified.inList != null) { - writer.println(" // TODO: in list $unified.inList") - } - - if (unified.persistentProperty.type == String && unified.creditCard) { - writer.println(' // TODO: must be credit card') - } - - if (unified.notEqual) { - writer.println(" // TODO: must not be equal to '$unified.notEqual'") - } - - if (unified.minSize != null && unified.maxSize != null) { - imports.add('javax.validation.constraints.Size') - writer.println(" @Size(min = $unified.minSize, max = $unified.maxSize)") - } else if (unified.maxSize != null) { - imports.add('javax.validation.constraints.Size') - writer.println(" @Size(max = $unified.maxSize)") - } else if (unified.minSize != null) { - imports.add('javax.validation.constraints.Size') - writer.println(" @Size(min = $unified.minSize)") - } - - if (unified.enumType) { - imports.add('javax.persistence.Enumerated') - imports.add('javax.persistence.EnumType') - writer.println(" @Enumerated(EnumType.$unified.enumType)") - } - - if (unified.sqlType || unified.sqlColumnName) { - imports.add('javax.persistence.Column') - writer.print(' @Column(') - if (unified.sqlType) { - writer.print("columnDefinition = '${unified.sqlType}'") - if (unified.sqlColumnName) { - writer.print(', ') - } - } - if (unified.sqlColumnName) { - writer.print("name = '${unified.sqlColumnName}'") - } - - writer.println(')') - } - - if (unified.persistentProperty.name == 'dateCreated') { - imports.add('io.micronaut.data.annotation.DateCreated') - writer.println(' @DateCreated') - } - - if (unified.persistentProperty.name == 'lastUpdated') { - imports.add('io.micronaut.data.annotation.DateUpdated') - writer.println(' @DateUpdated') - } - - writer.print(" $unified.persistentProperty.type.simpleName $unified.persistentProperty.name") - - Object defaultValue = unified.defaultValue - if (defaultValue != null && defaultValue != 0 && defaultValue != Boolean.FALSE) { - // further - column definition? - switch (defaultValue) { - case CharSequence: - writer.print " = '${defaultValue}'" - break - case Number: - case Boolean: - writer.print " = ${defaultValue}" - break - case Enum: - writer.print " = ${unified.persistentProperty.type.simpleName}.${defaultValue}" - break - case Date: - long delta = Math.abs(System.currentTimeMillis() - (defaultValue as Date).time) - if (delta < 1000) { - writer.print ' = new Date()' - } else { - writer.print " // TODO: defaults to '${defaultValue}'" - } - break - case Currency: - writer.print(" = Currency.getInstance('$defaultValue')") - break - case Locale.default: - writer.print(' = Locale.default') - break - case Locale.ENGLISH: - writer.print(' = Locale.ENGLISH') - break - case Locale: - Locale defaultLocale = defaultValue as Locale - writer.print(" = new Locale('${defaultLocale.language}', '${defaultLocale.country}')") - break - case TimeZone.default: - writer.print(' = TimeZone.default') - break - default: - writer.print " // TODO: defaults to '${defaultValue}'" - } - } - - writer.println() - writer.println() + super(datastore, constraintsEvaluator) } + @Override @SuppressWarnings('LineLength') - private static String generateRepository(PersistentEntity entity) { + protected String generateRepository(PersistentEntity entity) { String datasourceDefinition = '' if (entity.mapping.mappedForm.datasources) { @@ -510,45 +56,4 @@ class MicronautJpaGenerator { } """.stripIndent().trim() } - - private static void copyEnum(File root, Class enumType) { - if (!enumType.enum) { - return - } - - File packageDirectory = new File(root, enumType.package.name.replace('.', File.separator)) - packageDirectory.mkdirs() - - File enumFile = new File(packageDirectory, "${enumType.simpleName}.java") - - List enumValues = enumType.getMethod('values').invoke(null) as List - - enumFile.text = """ - package $enumType.package.name; - - public enum $enumType.simpleName { - // TODO: migrate original enum - ${enumValues.join(', ')}; - } - """.stripIndent().trim() - } - - @SuppressWarnings('ImplicitClosureParameter') - private List collectUnifiedProperties(PersistentEntity entity) { - Object newInstance = entity.newInstance() - Map constraintedProperties = constraintsEvaluator.evaluate(entity.javaClass) - List unifiedProperties = entity - .persistentProperties - .sort(false) { it.name } - .findAll { entity.identity?.name != it.name && entity.version?.name != it.name } - .collect { PersistentProperty property -> - ConstrainedProperty constrainedProperty = constraintedProperties.get(property.name) - Property mappingProperty = property.mapping.mappedForm as Property - Object value = property.reader.read(newInstance) - - return new UnifiedProperty(property, constrainedProperty, mappingProperty, value) - } - return unifiedProperties - } - } diff --git a/subprojects/micronaut-grails-jpa-generator/src/main/groovy/com/agorapulse/micronaut/grails/jpa/generator/MicronuatJpaGeneratorConfiguration.java b/subprojects/micronaut-grails-jpa-generator/src/main/groovy/com/agorapulse/micronaut/grails/jpa/generator/MicronuatDataGeneratorConfiguration.java similarity index 77% rename from subprojects/micronaut-grails-jpa-generator/src/main/groovy/com/agorapulse/micronaut/grails/jpa/generator/MicronuatJpaGeneratorConfiguration.java rename to subprojects/micronaut-grails-jpa-generator/src/main/groovy/com/agorapulse/micronaut/grails/jpa/generator/MicronuatDataGeneratorConfiguration.java index a2bfabf7..75fe4adf 100644 --- a/subprojects/micronaut-grails-jpa-generator/src/main/groovy/com/agorapulse/micronaut/grails/jpa/generator/MicronuatJpaGeneratorConfiguration.java +++ b/subprojects/micronaut-grails-jpa-generator/src/main/groovy/com/agorapulse/micronaut/grails/jpa/generator/MicronuatDataGeneratorConfiguration.java @@ -25,7 +25,7 @@ import javax.inject.Named; @Configuration -public class MicronuatJpaGeneratorConfiguration { +public class MicronuatDataGeneratorConfiguration { @Bean @Named("micronautJpaGenerator") @@ -36,4 +36,13 @@ public static MicronautJpaGenerator micronautJpaGenerator( return new MicronautJpaGenerator(datastore, constraintsEvaluator); } + @Bean + @Named("micronautJdbcGenerator") + public static MicronautJdbcGenerator micronautJdbcGenerator( + @Named("hibernateDatastore") Datastore datastore, + ConstraintsEvaluator constraintsEvaluator + ) { + return new MicronautJdbcGenerator(datastore, constraintsEvaluator); + } + } diff --git a/subprojects/micronaut-grails-jpa-generator/src/main/resources/META-INF/spring.factories b/subprojects/micronaut-grails-jpa-generator/src/main/resources/META-INF/spring.factories index 4c9cb05e..e84e64a4 100644 --- a/subprojects/micronaut-grails-jpa-generator/src/main/resources/META-INF/spring.factories +++ b/subprojects/micronaut-grails-jpa-generator/src/main/resources/META-INF/spring.factories @@ -1 +1 @@ -org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.agorapulse.micronaut.grails.jpa.generator.MicronuatJpaGeneratorConfiguration +org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.agorapulse.micronaut.grails.jpa.generator.MicronuatDataGeneratorConfiguration