diff --git a/src/main/java/org/openrewrite/java/migrate/javax/AddTransientAnnotationToEntity.java b/src/main/java/org/openrewrite/java/migrate/javax/AddTransientAnnotationToEntity.java new file mode 100644 index 0000000000..04f2140d8d --- /dev/null +++ b/src/main/java/org/openrewrite/java/migrate/javax/AddTransientAnnotationToEntity.java @@ -0,0 +1,114 @@ +/* + * 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.java.migrate.javax; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.openrewrite.*; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.JavaParser; +import org.openrewrite.java.JavaTemplate; +import org.openrewrite.java.search.FindAnnotations; +import org.openrewrite.java.search.UsesType; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaType; + +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; + +@Value +@EqualsAndHashCode(callSuper = false) +public class AddTransientAnnotationToEntity extends ScanningRecipe { + + @Override + public String getDisplayName() { + return "Unannotated entity attributes require a Transient annotation"; + } + + @Override + public String getDescription() { + return "In OpenJPA, attributes that are themselves entity classes are not persisted by default. EclipseLink has " + + "a different default behavior and tries to persist these attributes to the database. To keep the OpenJPA " + + "behavior of ignoring unannotated entity attributes, add the `javax.persistence.Transient` annotation to " + + "these attributes in EclipseLink."; + } + + static class EntityAccumulator { + private final Set entityClasses = new HashSet<>(); + + public void addEntity(JavaType type) { + entityClasses.add(type); + } + public boolean isEntity(JavaType type) { + return entityClasses.contains(type); + } + } + + @Override + public EntityAccumulator getInitialValue(ExecutionContext ctx) { + return new EntityAccumulator(); + } + + @Override + public TreeVisitor getScanner(EntityAccumulator acc) { + return Preconditions.check( + new UsesType<>("javax.persistence.Entity", true), + new JavaIsoVisitor() { + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) { + if (FindAnnotations.find(classDecl, "javax.persistence.Entity").isEmpty()) { + return classDecl; + } + // Collect @Entity classes + JavaType type = classDecl.getType(); + if (type != null) { + acc.addEntity(type); + } + return classDecl; + } + } + ); + } + + @Override + public TreeVisitor getVisitor(EntityAccumulator acc) { + return new JavaIsoVisitor() { + @Override + public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations multiVariable, ExecutionContext ctx) { + // Exit if attribute is not an Entity class + if (!acc.isEntity(multiVariable.getType())) { + return multiVariable; + } + // Exit if attribute is already JPA annotated + if (multiVariable.getLeadingAnnotations().stream() + .anyMatch(anno -> anno.getType().toString().contains("javax.persistence"))) { + return multiVariable; + } + // Add @Transient annotation + maybeAddImport("javax.persistence.Transient"); + return JavaTemplate.builder("@Transient") + .contextSensitive() + .javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "javax.persistence-api-2.2")) + .imports("javax.persistence.Transient") + .build() + .apply(getCursor(), multiVariable.getCoordinates().addAnnotation(Comparator.comparing(J.Annotation::getSimpleName))); + } + }; + } +} diff --git a/src/main/resources/META-INF/rewrite/openjpa-to-eclipselink.yml b/src/main/resources/META-INF/rewrite/openjpa-to-eclipselink.yml index 7b4163caf1..fe68a9824e 100644 --- a/src/main/resources/META-INF/rewrite/openjpa-to-eclipselink.yml +++ b/src/main/resources/META-INF/rewrite/openjpa-to-eclipselink.yml @@ -22,10 +22,12 @@ tags: - javaee7 - deprecated recipeList: - - org.openrewrite.java.migrate.javax.AddTableGenerator - org.openrewrite.java.migrate.javax.AddColumnAnnotation - org.openrewrite.java.migrate.javax.AddDefaultConstructorToEntityClass + - org.openrewrite.java.migrate.javax.AddTableGenerator - org.openrewrite.java.migrate.javax.AddTransientAnnotationToCollections + - org.openrewrite.java.migrate.javax.AddTransientAnnotationToEntity - org.openrewrite.java.migrate.javax.RemoveEmbeddableId - org.openrewrite.java.migrate.javax.RemoveTemporalAnnotation - - org.openrewrite.java.migrate.javax.UseJoinColumnForMapping \ No newline at end of file + - org.openrewrite.java.migrate.javax.UseJoinColumnForMapping + \ No newline at end of file diff --git a/src/test/java/org/openrewrite/java/migrate/javax/AddTransientAnnotationToEntityTest.java b/src/test/java/org/openrewrite/java/migrate/javax/AddTransientAnnotationToEntityTest.java new file mode 100644 index 0000000000..2efd9b9fa4 --- /dev/null +++ b/src/test/java/org/openrewrite/java/migrate/javax/AddTransientAnnotationToEntityTest.java @@ -0,0 +1,199 @@ +/* + * 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.java.migrate.javax; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.InMemoryExecutionContext; +import org.openrewrite.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +class AddTransientAnnotationToEntityTest implements RewriteTest { + @Override + public void defaults(RecipeSpec spec) { + spec.parser(JavaParser.fromJavaVersion().classpathFromResources(new InMemoryExecutionContext(), "javax.persistence-api-2.2")) + .recipe(new AddTransientAnnotationToEntity()); + } + + @Test + @DocumentExample + void addTransient() { + //language=java + rewriteRun( + java( + """ + import javax.persistence.Entity; + import javax.persistence.Id; + + @Entity + public class EntityA { + @Id + private int id; + + private EntityB entityReference; + } + """, + """ + import javax.persistence.Entity; + import javax.persistence.Id; + import javax.persistence.Transient; + + @Entity + public class EntityA { + @Id + private int id; + + @Transient + private EntityB entityReference; + } + """ + ), + java( + """ + import javax.persistence.Entity; + import javax.persistence.Id; + + @Entity + public class EntityB { + @Id + private int id; + } + """ + ) + ); + } + + @Test + void ignoreJpaAnnotatedEntity() { + //language=java + rewriteRun( + java( + """ + import javax.persistence.Entity; + import javax.persistence.Id; + + @Entity + public class EntityA { + @Id + private int id; + + @Id + private EntityB entityReference; + } + """ + ), + java( + """ + import javax.persistence.Entity; + import javax.persistence.Id; + + @Entity + public class EntityB { + @Id + private int id; + } + """ + ) + ); + } + + @Test + void addTransientOnNonJpaAnnotatedEntity() { + //language=java + rewriteRun( + java( + """ + import javax.persistence.Entity; + import javax.persistence.Id; + + import java.lang.annotation.Documented; + + @Entity + public class EntityA { + @Id + private int id; + + @Documented + private EntityB entityReference; + } + """, + """ + import javax.persistence.Entity; + import javax.persistence.Id; + import javax.persistence.Transient; + + import java.lang.annotation.Documented; + + @Entity + public class EntityA { + @Id + private int id; + + @Documented + @Transient + private EntityB entityReference; + } + """ + ), + java( + """ + import javax.persistence.Entity; + import javax.persistence.Id; + + @Entity + public class EntityB { + @Id + private int id; + } + """ + ) + ); + } + + @Test + void ignoreNonEntity() { + //language=java + rewriteRun( + java( + """ + import javax.persistence.Entity; + import javax.persistence.Id; + + @Entity + public class EntityA { + @Id + private int id; + + private NotEntityB entityReference; + } + """ + ), + java( + """ + import javax.persistence.Id; + + public class NotEntityB { + @Id + private int id; + } + """ + ) + ); + } +}