From 1fd6c84ce90300d900db59706183a04171a00867 Mon Sep 17 00:00:00 2001 From: Simon Zilliken <45996518+simonzn@users.noreply.github.com> Date: Mon, 19 Feb 2024 13:18:58 +0100 Subject: [PATCH] Adds recipe for migration of @LazyCollection (#17) * Adds recipe for migration of @LazyCollection @LazyCollection is deprecated since Hibernate 6.2 Added a recipe which removes the annotation and sets the fetch type in jakarta.persistence annotations instead. * Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Revert "Apply suggestions from code review" This reverts commit fff71fc06332d51c30cd198d89a75bcf051957f7. * Minimize text block indentation and add code highlighting * Add precondition to limit recipe applicability * No need to finalize local fields * Remove need to construct J.FieldAccess * Use `FindAnnotation` and `RemoveAnnotationVisitor` * Apply suggestions from code review * Eliminates jakarta.persistence-api runtime dependency * At least set type correctly --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Tim te Beek Co-authored-by: Tim te Beek --- build.gradle.kts | 2 +- .../ReplaceLazyCollectionAnnotation.java | 145 +++++ .../ReplaceLazyCollectionAnnotationTest.java | 495 ++++++++++++++++++ 3 files changed, 641 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/openrewrite/hibernate/ReplaceLazyCollectionAnnotation.java create mode 100644 src/test/java/org/openrewrite/hibernate/ReplaceLazyCollectionAnnotationTest.java diff --git a/build.gradle.kts b/build.gradle.kts index c220677..8dc20b9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -20,5 +20,5 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter-engine:latest.release") testRuntimeOnly("org.hibernate:hibernate-core:5.6.15.Final") - + testRuntimeOnly("jakarta.persistence:jakarta.persistence-api:3.1.0") } diff --git a/src/main/java/org/openrewrite/hibernate/ReplaceLazyCollectionAnnotation.java b/src/main/java/org/openrewrite/hibernate/ReplaceLazyCollectionAnnotation.java new file mode 100644 index 0000000..bc9e260 --- /dev/null +++ b/src/main/java/org/openrewrite/hibernate/ReplaceLazyCollectionAnnotation.java @@ -0,0 +1,145 @@ +/* + * 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.internal.ListUtils; +import org.openrewrite.java.AnnotationMatcher; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.JavaTemplate; +import org.openrewrite.java.RemoveAnnotationVisitor; +import org.openrewrite.java.search.FindAnnotations; +import org.openrewrite.java.search.UsesType; +import org.openrewrite.java.tree.*; + +import java.util.List; +import java.util.Optional; + +public class ReplaceLazyCollectionAnnotation extends Recipe { + @Override + public String getDisplayName() { + return "Replace `@LazyCollection` with `jakarta.persistence.FetchType`"; + } + + @Override + public String getDescription() { + return "Adds the `FetchType` to jakarta annotations and deletes `@LazyCollection`."; + } + + @Override + public TreeVisitor getVisitor() { + return Preconditions.check(new UsesType<>("org.hibernate.annotations.LazyCollection", true), new JavaIsoVisitor() { + @Override + public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext ctx) { + maybeRemoveImport("org.hibernate.annotations.LazyCollection"); + maybeRemoveImport("org.hibernate.annotations.LazyCollectionOption"); + return super.visitCompilationUnit(cu, ctx); + } + + @Override + public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) { + J.MethodDeclaration j = removeLazyCollectionAnnotation(method, ctx); + return super.visitMethodDeclaration(j, ctx); + } + + @Override + public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations multiVariable, + ExecutionContext ctx) { + J.VariableDeclarations j = removeLazyCollectionAnnotation(multiVariable, ctx); + return super.visitVariableDeclarations(j, ctx); + } + + @Override + public J.Annotation visitAnnotation(J.Annotation annotation, ExecutionContext ctx) { + J.Annotation ann = super.visitAnnotation(annotation, ctx); + + JavaType annType = ann.getType(); + if (!(TypeUtils.isOfClassType(annType, "jakarta.persistence.ElementCollection") || + TypeUtils.isOfClassType(annType, "jakarta.persistence.OneToOne") || + TypeUtils.isOfClassType(annType, "jakarta.persistence.OneToMany") || + TypeUtils.isOfClassType(annType, "jakarta.persistence.ManyToOne") || + TypeUtils.isOfClassType(annType, "jakarta.persistence.ManyToMany"))) { + // recipe does not apply + return ann; + } + + List currentArgs = ann.getArguments(); + + // Do not update existing fetch value + if (currentArgs != null && currentArgs.stream() + .filter(arg -> arg instanceof J.Assignment) + .map(J.Assignment.class::cast) + .anyMatch(arg -> ((J.Identifier) arg.getVariable()).getSimpleName().equals("fetch"))) { + return ann; + } + + // Retrieve FetchType set from LazyCollectionOption + String fetchType = getCursor().getNearestMessage("fetchType"); + if (fetchType == null) { + // no mapping found + return ann; + } + + maybeAddImport("jakarta.persistence.FetchType", false); + J.Annotation annotationWithFetch = JavaTemplate.builder("fetch = " + fetchType) + .imports("jakarta.persistence.FetchType") + .contextSensitive() + .build() + .apply(getCursor(), ann.getCoordinates().replaceArguments()); + JavaType fetchTypeType = JavaType.buildType("jakarta.persistence.FetchType"); + J.Assignment assignment = (J.Assignment) annotationWithFetch.getArguments().get(0); + assignment = assignment + .withPrefix(currentArgs == null || currentArgs.isEmpty() ? Space.EMPTY : Space.SINGLE_SPACE) + .withAssignment(assignment.getAssignment().withType(fetchTypeType)) + .withType(fetchTypeType); + return ann.withArguments(ListUtils.concat(currentArgs, assignment)); + } + + private T removeLazyCollectionAnnotation(T tree, ExecutionContext ctx) { + Optional lazyAnnotation = FindAnnotations.find(tree, "org.hibernate.annotations.LazyCollection") + .stream().findFirst(); + if (!lazyAnnotation.isPresent()) { + return tree; + } + + // Capture the FetchType from the LazyCollectionOption + List arguments = lazyAnnotation.get().getArguments(); + if (arguments == null || arguments.isEmpty()) { + // default is LazyCollectionOption.TRUE + getCursor().putMessage("fetchType", "FetchType.LAZY"); + } else { + switch (arguments.get(0).toString()) { + case "LazyCollectionOption.FALSE": + getCursor().putMessage("fetchType", "FetchType.EAGER"); + break; + case "LazyCollectionOption.TRUE": + getCursor().putMessage("fetchType", "FetchType.LAZY"); + break; + default: + // EXTRA can't be mapped to a FetchType; requires refactoring + return tree; + } + } + //noinspection unchecked + return (T) new RemoveAnnotationVisitor(new AnnotationMatcher("@org.hibernate.annotations.LazyCollection")) + .visit(tree, ctx, getCursor()); + } + }); + } +} diff --git a/src/test/java/org/openrewrite/hibernate/ReplaceLazyCollectionAnnotationTest.java b/src/test/java/org/openrewrite/hibernate/ReplaceLazyCollectionAnnotationTest.java new file mode 100644 index 0000000..2f301d3 --- /dev/null +++ b/src/test/java/org/openrewrite/hibernate/ReplaceLazyCollectionAnnotationTest.java @@ -0,0 +1,495 @@ +/* + * 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 ReplaceLazyCollectionAnnotationTest implements RewriteTest { + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new ReplaceLazyCollectionAnnotation()) + .parser(JavaParser.fromJavaVersion() + .classpath("hibernate-core", "jakarta.persistence-api") + ); + } + + @ParameterizedTest + @CsvSource({ + //"LazyCollectionOption.FALSE, FetchType.EAGER, ElementCollection", // different import order + "LazyCollectionOption.FALSE, FetchType.EAGER, ManyToMany", + "LazyCollectionOption.FALSE, FetchType.EAGER, OneToMany", + "LazyCollectionOption.TRUE, FetchType.LAZY, OneToOne", + "LazyCollectionOption.TRUE, FetchType.LAZY, ManyToOne" + }) + void methodAnnotation_shouldBeUpdated_whenFetchArgumentIsMissing( + String oldArg, String newArg, String targetAnnotation) { + //language=java + rewriteRun( + java( + """ + import org.hibernate.annotations.LazyCollection; + import org.hibernate.annotations.LazyCollectionOption; + import jakarta.persistence.%1$s; + + import java.util.HashSet; + import java.util.Set; + + class SomeClass { + + private Set items = new HashSet<>(); + + @LazyCollection(%2$s) + @%1$s + public Set getItems() { + return items; + } + } + """.formatted(targetAnnotation, oldArg), + """ + import jakarta.persistence.FetchType; + import jakarta.persistence.%1$s; + + import java.util.HashSet; + import java.util.Set; + + class SomeClass { + + private Set items = new HashSet<>(); + + @%1$s(fetch = %2$s) + public Set getItems() { + return items; + } + } + """.formatted(targetAnnotation, newArg) + ) + ); + } + + @ParameterizedTest + @CsvSource({ + //"LazyCollectionOption.FALSE, FetchType.EAGER, ElementCollection", // different import order + "LazyCollectionOption.FALSE, FetchType.EAGER, ManyToMany", + "LazyCollectionOption.FALSE, FetchType.EAGER, OneToMany", + "LazyCollectionOption.TRUE, FetchType.LAZY, OneToOne", + "LazyCollectionOption.TRUE, FetchType.LAZY, ManyToOne" + }) + void fieldAnnotation_shouldBeUpdated_whenFetchArgumentIsMissing( + String oldArg, String newArg, String targetAnnotation) { + //language=java + rewriteRun( + java( + """ + import org.hibernate.annotations.LazyCollection; + import org.hibernate.annotations.LazyCollectionOption; + import jakarta.persistence.%1$s; + + import java.util.List; + + class SomeClass { + + @LazyCollection(%2$s) + @%1$s + private List items; + } + """.formatted(targetAnnotation, oldArg), + """ + import jakarta.persistence.FetchType; + import jakarta.persistence.%1$s; + + import java.util.List; + + class SomeClass { + + @%1$s(fetch = %2$s) + private List items; + } + """.formatted(targetAnnotation, newArg) + ) + ); + } + + @Test + void methodAnnotation_shouldNotBeUpdated_whenFetchArgumentIsPresent() { + //language=java + rewriteRun( + // The before class makes no sense. This is intentional - the test case demonstrates + // that the fetch argument is not changed, even if it doesn't match the old + // LazyCollectionOption + java( + """ + import org.hibernate.annotations.LazyCollection; + import org.hibernate.annotations.LazyCollectionOption; + import jakarta.persistence.FetchType; + import jakarta.persistence.ManyToMany; + + import java.util.HashSet; + import java.util.Set; + + class SomeClass { + + private Set items = new HashSet<>(); + + @LazyCollection(LazyCollectionOption.FALSE) + @ManyToMany(fetch = FetchType.LAZY) + public Set getItems() { + return items; + } + } + """, + """ + import jakarta.persistence.FetchType; + import jakarta.persistence.ManyToMany; + + import java.util.HashSet; + import java.util.Set; + + class SomeClass { + + private Set items = new HashSet<>(); + + @ManyToMany(fetch = FetchType.LAZY) + public Set getItems() { + return items; + } + } + """ + ) + ); + } + + @Test + void fieldAnnotation_shouldNotBeUpdated_whenFetchArgumentIsPresent() { + //language=java + rewriteRun( + // The before class makes no sense. This is intentional - the test case demonstrates + // that the fetch argument is not changed, even if it doesn't match the old + // LazyCollectionOption + java( + """ + import org.hibernate.annotations.LazyCollection; + import org.hibernate.annotations.LazyCollectionOption; + import jakarta.persistence.ElementCollection; + import jakarta.persistence.FetchType; + + import java.util.HashSet; + import java.util.Set; + + class SomeClass { + + @LazyCollection(LazyCollectionOption.FALSE) + @ElementCollection(fetch = FetchType.LAZY) + private Set items; + } + """, + """ + import jakarta.persistence.ElementCollection; + import jakarta.persistence.FetchType; + + import java.util.HashSet; + import java.util.Set; + + class SomeClass { + + @ElementCollection(fetch = FetchType.LAZY) + private Set items; + } + """ + ) + ); + } + + @Test + void methodAnnotation_shouldNotBeUpdated_whenLazyCollectionOptionIsExtra() { + //language=java + rewriteRun( + java( + """ + import org.hibernate.annotations.LazyCollection; + import org.hibernate.annotations.LazyCollectionOption; + import jakarta.persistence.ElementCollection; + + import java.util.HashSet; + import java.util.Set; + + class SomeClass { + + private Set items; + + @LazyCollection(LazyCollectionOption.EXTRA) + @ElementCollection + public Set getItems() { + return items; + } + } + """ + ) + ); + } + + @Test + void fieldAnnotation_shouldNotBeUpdated_whenLazyCollectionOptionIsExtra() { + //language=java + rewriteRun( + java( + """ + import org.hibernate.annotations.LazyCollection; + import org.hibernate.annotations.LazyCollectionOption; + import jakarta.persistence.ElementCollection; + + import java.util.HashSet; + import java.util.Set; + + class SomeClass { + + @LazyCollection(LazyCollectionOption.EXTRA) + @ElementCollection + private Set items; + } + """ + ) + ); + } + + @DocumentExample + @Test + void methodAnnotation_shouldKeepPreviousArguments_whenFetchTypeIsAdded() { + //language=java + rewriteRun( + java( + """ + import org.hibernate.annotations.LazyCollection; + import org.hibernate.annotations.LazyCollectionOption; + import jakarta.persistence.CascadeType; + import jakarta.persistence.OneToMany; + + import java.util.HashSet; + import java.util.Set; + + class SomeClass { + + private Set items = new HashSet<>(); + + @LazyCollection(LazyCollectionOption.FALSE) + @OneToMany(cascade = { CascadeType.PERSIST, CascadeType.REFRESH, CascadeType.MERGE }) + public Set getItems() { + return items; + } + } + """, + """ + import jakarta.persistence.CascadeType; + import jakarta.persistence.FetchType; + import jakarta.persistence.OneToMany; + + import java.util.HashSet; + import java.util.Set; + + class SomeClass { + + private Set items = new HashSet<>(); + + @OneToMany(cascade = { CascadeType.PERSIST, CascadeType.REFRESH, CascadeType.MERGE }, fetch = FetchType.EAGER) + public Set getItems() { + return items; + } + } + """ + ) + ); + } + + @Test + void fieldAnnotation_shouldKeepPreviousArguments_whenFetchTypeIsAdded() { + //language=java + rewriteRun( + java( + """ + import org.hibernate.annotations.LazyCollection; + import org.hibernate.annotations.LazyCollectionOption; + import jakarta.persistence.CascadeType; + import jakarta.persistence.OneToMany; + + import java.util.HashSet; + import java.util.Set; + + class SomeClass { + + @LazyCollection(LazyCollectionOption.FALSE) + @OneToMany(cascade = { CascadeType.PERSIST, CascadeType.REFRESH, CascadeType.MERGE }) + private Set items = new HashSet<>(); + } + """, + """ + import jakarta.persistence.CascadeType; + import jakarta.persistence.FetchType; + import jakarta.persistence.OneToMany; + + import java.util.HashSet; + import java.util.Set; + + class SomeClass { + + @OneToMany(cascade = { CascadeType.PERSIST, CascadeType.REFRESH, CascadeType.MERGE }, fetch = FetchType.EAGER) + private Set items = new HashSet<>(); + } + """ + ) + ); + } + + @Test + void allLazyCollectionAnnotationsInClass_ShouldBeProcessed() { + //language=java + rewriteRun( + java( + """ + import org.hibernate.annotations.LazyCollection; + import org.hibernate.annotations.LazyCollectionOption; + import jakarta.persistence.ElementCollection; + import jakarta.persistence.ManyToMany; + + import java.util.HashSet; + import java.util.Set; + + class SomeClass { + + private Set items1; + private Set items2; + + @ElementCollection + @LazyCollection(LazyCollectionOption.EXTRA) + private Set items3; + + @LazyCollection + @ElementCollection + private Set items4; + + @ManyToMany + @LazyCollection(LazyCollectionOption.TRUE) + public Set getItems1() { + return items1; + } + + @LazyCollection(LazyCollectionOption.FALSE) + @ManyToMany + public Set getItems2() { + return items2; + } + } + """, + """ + import org.hibernate.annotations.LazyCollection; + import org.hibernate.annotations.LazyCollectionOption; + import jakarta.persistence.ElementCollection; + import jakarta.persistence.FetchType; + import jakarta.persistence.ManyToMany; + + import java.util.HashSet; + import java.util.Set; + + class SomeClass { + + private Set items1; + private Set items2; + + @ElementCollection + @LazyCollection(LazyCollectionOption.EXTRA) + private Set items3; + + @ElementCollection(fetch = FetchType.LAZY) + private Set items4; + + @ManyToMany(fetch = FetchType.LAZY) + public Set getItems1() { + return items1; + } + + @ManyToMany(fetch = FetchType.EAGER) + public Set getItems2() { + return items2; + } + } + """ + ) + ); + } + + @Test + void regression_indentationIsWrong_whenSeveralAnnotationsArePresent() { + //language=java + rewriteRun( + java( + """ + import org.hibernate.annotations.LazyCollection; + import org.hibernate.annotations.LazyCollectionOption; + import org.hibernate.annotations.Cascade; + import org.hibernate.annotations.CascadeType; + import jakarta.persistence.JoinColumn; + import jakarta.persistence.JoinTable; + import jakarta.persistence.ManyToMany; + + import java.util.HashSet; + import java.util.Set; + + class SomeClass { + + private Set items = new HashSet<>(); + + @Cascade(CascadeType.MERGE) + @LazyCollection(LazyCollectionOption.FALSE) + @ManyToMany + @JoinTable(name = "A_B", joinColumns = @JoinColumn(name = "A_ID"), inverseJoinColumns = @JoinColumn(name = "B_ID")) + public Set getItems() { + return items; + } + } + """, + """ + import org.hibernate.annotations.Cascade; + import org.hibernate.annotations.CascadeType; + import jakarta.persistence.FetchType; + import jakarta.persistence.JoinColumn; + import jakarta.persistence.JoinTable; + import jakarta.persistence.ManyToMany; + + import java.util.HashSet; + import java.util.Set; + + class SomeClass { + + private Set items = new HashSet<>(); + + @Cascade(CascadeType.MERGE) + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable(name = "A_B", joinColumns = @JoinColumn(name = "A_ID"), inverseJoinColumns = @JoinColumn(name = "B_ID")) + public Set getItems() { + return items; + } + } + """ + ) + ); + } +}