diff --git a/build.gradle.kts b/build.gradle.kts index 8dc20b9..c0d9636 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,6 +19,8 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter-engine:latest.release") - testRuntimeOnly("org.hibernate:hibernate-core:5.6.15.Final") + testRuntimeOnly("org.hibernate:hibernate-core:6.4.4.Final") + testRuntimeOnly("javax.xml.bind:jaxb-api:2.3.1") + testRuntimeOnly("javax.persistence:javax.persistence-api:2.2") testRuntimeOnly("jakarta.persistence:jakarta.persistence-api:3.1.0") } diff --git a/src/main/java/org/openrewrite/hibernate/MigrateBooleanMappings.java b/src/main/java/org/openrewrite/hibernate/MigrateBooleanMappings.java new file mode 100644 index 0000000..5997a67 --- /dev/null +++ b/src/main/java/org/openrewrite/hibernate/MigrateBooleanMappings.java @@ -0,0 +1,112 @@ +/* + * Copyright 2024 the original author or authors. + *

+ * 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 org.openrewrite.hibernate; + +import org.openrewrite.ExecutionContext; +import org.openrewrite.Preconditions; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.JavaParser; +import org.openrewrite.java.JavaTemplate; +import org.openrewrite.java.search.UsesType; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.TypeUtils; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class MigrateBooleanMappings extends Recipe { + + private static final Map REPLACEMENTS = new HashMap<>(); + + static { + REPLACEMENTS.put("org.hibernate.type.TrueFalseBooleanType", "TrueFalseConverter"); + REPLACEMENTS.put("true_false", "TrueFalseConverter"); + REPLACEMENTS.put("org.hibernate.type.YesNoBooleanType", "YesNoConverter"); + REPLACEMENTS.put("yes_no", "YesNoConverter"); + REPLACEMENTS.put("org.hibernate.type.NumericBooleanType", "NumericBooleanConverter"); + REPLACEMENTS.put("numeric_boolean", "NumericBooleanConverter"); + } + + @Override + public String getDisplayName() { + return "Replace boolean type mappings with converters"; + } + + @Override + public String getDescription() { + return "Replaces type mapping of booleans with appropriate attribute converters."; + } + + @Override + public TreeVisitor getVisitor() { + return Preconditions.check(new UsesType<>("org.hibernate.annotations.Type", true), + new JavaIsoVisitor() { + @Override + public J.Annotation visitAnnotation(J.Annotation annotation, ExecutionContext ctx) { + J.Annotation ann = super.visitAnnotation(annotation, ctx); + if (!TypeUtils.isOfClassType(ann.getType(), "org.hibernate.annotations.Type")) { + return ann; + } + + List args = ann.getArguments(); + if (args == null) { + return ann; + } + + Object type = args.stream() + .filter(exp -> { + if (exp instanceof J.Assignment) { + J.Identifier variable = (J.Identifier) ((J.Assignment) exp).getVariable(); + return "type".equals(variable.getSimpleName()); + } + return false; + }) + .findFirst() + .map(exp -> { + Expression value = ((J.Assignment) exp).getAssignment(); + if (value instanceof J.Literal) { + return ((J.Literal) value).getValue(); + } + return null; + }) + .orElse(null); + + if (type instanceof String && REPLACEMENTS.containsKey((String) type)) { + String converterName = REPLACEMENTS.get((String) type); + String converterFQN = String.format("org.hibernate.type.%s", converterName); + + ann = JavaTemplate.builder(String.format("@Convert(converter = %s.class)", converterName)) + .javaParser(JavaParser.fromJavaVersion().classpath("hibernate-core", "jakarta.persistence-api")) + .imports(converterFQN, "jakarta.persistence.Convert") + .contextSensitive() + .build().apply(getCursor(), ann.getCoordinates().replace()); + + maybeAddImport("jakarta.persistence.Convert"); + maybeAddImport(converterFQN); + maybeRemoveImport("org.hibernate.annotations.Type"); + } + + return ann; + } + } + ); + } +} diff --git a/src/main/resources/META-INF/rewrite/hibernate-6.yml b/src/main/resources/META-INF/rewrite/hibernate-6.yml index 521dc9f..bc419ee 100644 --- a/src/main/resources/META-INF/rewrite/hibernate-6.yml +++ b/src/main/resources/META-INF/rewrite/hibernate-6.yml @@ -25,6 +25,7 @@ description: > recipeList: - org.openrewrite.hibernate.MigrateToHibernateDependencies61 + - org.openrewrite.hibernate.MigrateBooleanMappings - org.openrewrite.hibernate.TypeAnnotationParameter - org.openrewrite.hibernate.TypeDescriptorToType - org.openrewrite.java.migrate.jakarta.JavaxPersistenceToJakartaPersistence diff --git a/src/test/java/org/openrewrite/hibernate/MigrateBooleanMappingsTest.java b/src/test/java/org/openrewrite/hibernate/MigrateBooleanMappingsTest.java new file mode 100644 index 0000000..9a43a0c --- /dev/null +++ b/src/test/java/org/openrewrite/hibernate/MigrateBooleanMappingsTest.java @@ -0,0 +1,301 @@ +/* + * Copyright 2024 the original author or authors. + *

+ * 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 org.openrewrite.hibernate; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.openrewrite.DocumentExample; +import org.openrewrite.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +class MigrateBooleanMappingsTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new MigrateBooleanMappings()) + .parser(JavaParser.fromJavaVersion() + .classpath("hibernate-core", "jakarta.persistence-api") + ); + } + + @DocumentExample + @Test + void allMappings_shouldBeReplaced() { + //language=java + rewriteRun( + java( + """ + import jakarta.persistence.Column; + import org.hibernate.annotations.Type; + + public class SomeClass { + + @Column(name = "IS_SOMETHING") + @Type(type = "true_false") + private boolean isSomething; + + @Column(name = "IS_SOMETHING_ELSE") + @Type(type = "org.hibernate.type.YesNoBooleanType") + private boolean isSomethingElse; + + } + """, + """ + import jakarta.persistence.Column; + import jakarta.persistence.Convert; + import org.hibernate.type.TrueFalseConverter; + import org.hibernate.type.YesNoConverter; + + public class SomeClass { + + @Column(name = "IS_SOMETHING") + @Convert(converter = TrueFalseConverter.class) + private boolean isSomething; + + @Column(name = "IS_SOMETHING_ELSE") + @Convert(converter = YesNoConverter.class) + private boolean isSomethingElse; + + } + """ + ) + ); + } + + @ParameterizedTest + @CsvSource(textBlock = """ + numeric_boolean , NumericBooleanConverter + true_false , TrueFalseConverter + yes_no , YesNoConverter + org.hibernate.type.YesNoBooleanType , YesNoConverter + org.hibernate.type.TrueFalseBooleanType , TrueFalseConverter + org.hibernate.type.NumericBooleanType , NumericBooleanConverter + """) + void mapping_shouldBeReplaced_whenMethodIsAnnotated(String usertype, String converter) { + //language=java + rewriteRun( + java( + """ + import jakarta.persistence.Column; + import org.hibernate.annotations.Type; + + public class SomeClass { + + private boolean isSomething; + + @Column(name = "IS_SOMETHING") + @Type(type = "%s") + public boolean isSomething() { + return isSomething; + } + } + """.formatted(usertype), + """ + import jakarta.persistence.Column; + import jakarta.persistence.Convert; + import org.hibernate.type.%1$s; + + public class SomeClass { + + private boolean isSomething; + + @Column(name = "IS_SOMETHING") + @Convert(converter = %1$s.class) + public boolean isSomething() { + return isSomething; + } + } + """.formatted(converter) + ) + ); + } + + @ParameterizedTest + @CsvSource(textBlock = """ + numeric_boolean , NumericBooleanConverter + true_false , TrueFalseConverter + yes_no , YesNoConverter + org.hibernate.type.YesNoBooleanType , YesNoConverter + org.hibernate.type.TrueFalseBooleanType , TrueFalseConverter + org.hibernate.type.NumericBooleanType , NumericBooleanConverter + """) + void trueFalseMapping_shouldBeReplaced_whenFieldIsAnnotated(String usertype, String converter) { + //language=java + rewriteRun( + java( + """ + import jakarta.persistence.Column; + import org.hibernate.annotations.Type; + + public class SomeClass { + + @Column(name = "IS_SOMETHING") + @Type(type = "%s") + private boolean isSomething; + + public boolean isSomething() { + return isSomething; + } + } + """.formatted(usertype), + """ + import jakarta.persistence.Column; + import jakarta.persistence.Convert; + import org.hibernate.type.%1$s; + + public class SomeClass { + + @Column(name = "IS_SOMETHING") + @Convert(converter = %1$s.class) + private boolean isSomething; + + public boolean isSomething() { + return isSomething; + } + } + """.formatted(converter) + ) + ); + } + + @Test + void typeImport_shouldNotBeRemoved_ifUsedElsewhere() { + //language=java + rewriteRun( + java( + """ + import org.hibernate.annotations.Type; + import jakarta.persistence.Column; + + public class SomeClass { + + private boolean isSomething; + private Object someObject; + + @Column(name = "IS_SOMETHING") + @Type(type = "true_false") + public boolean isSomething() { + return isSomething; + } + + @Column(name = "SOME_OBJECT") + @Type(type = Object.class) + public Object getSomeObject() { + return someObject; + } + } + """, + """ + import jakarta.persistence.Convert; + import org.hibernate.annotations.Type; + import org.hibernate.type.TrueFalseConverter; + import jakarta.persistence.Column; + + public class SomeClass { + + private boolean isSomething; + private Object someObject; + + @Column(name = "IS_SOMETHING") + @Convert(converter = TrueFalseConverter.class) + public boolean isSomething() { + return isSomething; + } + + @Column(name = "SOME_OBJECT") + @Type(type = Object.class) + public Object getSomeObject() { + return someObject; + } + } + """ + ) + ); + } + + @Test + void noChange_shouldBeMade_whenTypeIsClass() { + //language=java + rewriteRun( + java( + """ + import org.hibernate.annotations.Type; + import jakarta.persistence.Column; + + public class SomeClass { + + private Object someObject; + + @Column(name = "SOME_OBJECT") + @Type(type = Object.class) // we just need some class, it is not checked + public Object getSomeObject() { + return someObject; + } + } + """ + ) + ); + } + + @Test + void noChange_shouldBeMade_whenTypeIsNameOfClass() { + //language=java + rewriteRun( + java( + """ + import org.hibernate.annotations.Type; + import jakarta.persistence.Column; + + public class SomeClass { + + private Object someObject; + + @Column(name = "SOME_OBJECT") + @Type(type = "java.lang.Object") // we just need some class name, it is not checked + public Object getSomeObject() { + return someObject; + } + } + """ + ) + ); + } + + @Test + void noChange_shouldBeMade_whenTypeIsBoolean() { + //language=java + rewriteRun( + java( + """ + import jakarta.persistence.Column; + import org.hibernate.annotations.Type; + + public class SomeClass { + + @Column(name = "IS_SOMETHING") + @Type(type = "boolean") + private boolean isSomething; + } + """ + ) + ); + } + +}