diff --git a/src/main/java/org/openrewrite/java/migrate/javax/AddTransientAnnotationToCollections.java b/src/main/java/org/openrewrite/java/migrate/javax/AddTransientAnnotationToCollections.java new file mode 100644 index 0000000000..32ebbd6ffb --- /dev/null +++ b/src/main/java/org/openrewrite/java/migrate/javax/AddTransientAnnotationToCollections.java @@ -0,0 +1,85 @@ +/* + * 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.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.J; + +import java.util.Comparator; +import java.util.regex.Pattern; + +@Value +@EqualsAndHashCode(callSuper = false) +public class AddTransientAnnotationToCollections extends Recipe { + + @Override + public String getDisplayName() { + return "Unannotated collection attributes require a Transient annotation"; + } + + @Override + public String getDescription() { + return "In OpenJPA, attributes that inherit from the `java.util.Collection` interface are not a default " + + "persistent type, so these attributes are not persisted unless they are annotated. EclipseLink has a " + + "different default behavior and attempts to persist these attributes to the database. To keep the OpenJPA " + + "behavior of ignoring unannotated collection attributes, add the `javax.persistence.Transient` annotation " + + "to these attributes in EclipseLink."; + } + + @Override + public TreeVisitor getVisitor() { + Pattern collection = Pattern.compile("java.util.Collection"); + return Preconditions.check( + // Only apply to JPA classes + Preconditions.or( + new UsesType<>("javax.persistence.Entity", true), + new UsesType<>("javax.persistence.MappedSuperclass", true), + new UsesType<>("javax.persistence.Embeddable", true) + ), + new JavaIsoVisitor() { + @Override + public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations multiVariable, ExecutionContext ctx) { + // Exit if not Collection + if (!multiVariable.getType().isAssignableFrom(collection)) { + return multiVariable; + } + // Exit if already has JPA annotation + 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 0b78a11841..7b4163caf1 100644 --- a/src/main/resources/META-INF/rewrite/openjpa-to-eclipselink.yml +++ b/src/main/resources/META-INF/rewrite/openjpa-to-eclipselink.yml @@ -25,7 +25,7 @@ recipeList: - org.openrewrite.java.migrate.javax.AddTableGenerator - org.openrewrite.java.migrate.javax.AddColumnAnnotation - org.openrewrite.java.migrate.javax.AddDefaultConstructorToEntityClass + - org.openrewrite.java.migrate.javax.AddTransientAnnotationToCollections - 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/AddTransientAnnotationToCollectionsTest.java b/src/test/java/org/openrewrite/java/migrate/javax/AddTransientAnnotationToCollectionsTest.java new file mode 100644 index 0000000000..c1613e9719 --- /dev/null +++ b/src/test/java/org/openrewrite/java/migrate/javax/AddTransientAnnotationToCollectionsTest.java @@ -0,0 +1,303 @@ +/* + * 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 AddTransientAnnotationToCollectionsTest implements RewriteTest { + @Override + public void defaults(RecipeSpec spec) { + spec.parser(JavaParser.fromJavaVersion().classpathFromResources(new InMemoryExecutionContext(), "javax.persistence-api-2.2")) + .recipe(new AddTransientAnnotationToCollections()); + } + + @Test + @DocumentExample + void addTransient() { + //language=java + rewriteRun( + java( + """ + import java.util.Collection; + import java.util.List; + + import javax.persistence.Entity; + import javax.persistence.Id; + + @Entity + public class UnannotatedCollectionEntity { + @Id + private int id; + + private Collection collectionField; + private List listField; + } + """, + """ + import java.util.Collection; + import java.util.List; + + import javax.persistence.Entity; + import javax.persistence.Id; + import javax.persistence.Transient; + + @Entity + public class UnannotatedCollectionEntity { + @Id + private int id; + + @Transient + private Collection collectionField; + @Transient + private List listField; + } + """ + ) + ); + } + + @Test + void ignoreJpaAnnotatedCollection() { + //language=java + rewriteRun( + java( + """ + import java.util.Collection; + import java.util.List; + + import javax.persistence.Entity; + import javax.persistence.Id; + + @Entity + public class UnannotatedCollectionEntity { + @Id + private int id; + + @Id + private Collection collectionField; + private List listField; + } + """, + """ + import java.util.Collection; + import java.util.List; + + import javax.persistence.Entity; + import javax.persistence.Id; + import javax.persistence.Transient; + + @Entity + public class UnannotatedCollectionEntity { + @Id + private int id; + + @Id + private Collection collectionField; + @Transient + private List listField; + } + """ + ) + ); + } + + @Test + void addTransientToNonJpaAnnotatedCollection() { + //language=java + rewriteRun( + java( + """ + import java.util.Collection; + import java.util.List; + import java.lang.annotation.Documented; + + import javax.persistence.Entity; + import javax.persistence.Id; + + @Entity + public class UnannotatedCollectionEntity { + @Id + private int id; + + @Documented + private Collection collectionField; + private List listField; + } + """, + """ + import java.util.Collection; + import java.util.List; + import java.lang.annotation.Documented; + + import javax.persistence.Entity; + import javax.persistence.Id; + import javax.persistence.Transient; + + @Entity + public class UnannotatedCollectionEntity { + @Id + private int id; + + @Documented + @Transient + private Collection collectionField; + @Transient + private List listField; + } + """ + ) + ); + } + + @Test + void variousCollections() { + //language=java + rewriteRun( + java( + """ + import java.util.Collection; + import java.util.List; + + import javax.persistence.Entity; + import javax.persistence.Id; + + @Entity + public class UnannotatedCollectionEntity { + @Id + private int id; + private String string; + private String[] arrayString; + + private Collection collectionField; + private List listField; + private java.beans.beancontext.BeanContext beanContext; + private java.beans.beancontext.BeanContextServices beanCServices; + private java.util.concurrent.BlockingDeque bdeque; + private java.util.concurrent.BlockingQueue bqueue; + private java.util.Deque deque; + private java.util.List list; + private java.util.NavigableSet navSet; + private java.util.Queue queue; + private java.util.Set set; + private java.util.SortedSet sortedSet; + private java.util.concurrent.TransferQueue transferQueue; + } + """, + """ + import java.util.Collection; + import java.util.List; + + import javax.persistence.Entity; + import javax.persistence.Id; + import javax.persistence.Transient; + + @Entity + public class UnannotatedCollectionEntity { + @Id + private int id; + private String string; + private String[] arrayString; + + @Transient + private Collection collectionField; + @Transient + private List listField; + @Transient + private java.beans.beancontext.BeanContext beanContext; + @Transient + private java.beans.beancontext.BeanContextServices beanCServices; + @Transient + private java.util.concurrent.BlockingDeque bdeque; + @Transient + private java.util.concurrent.BlockingQueue bqueue; + @Transient + private java.util.Deque deque; + @Transient + private java.util.List list; + @Transient + private java.util.NavigableSet navSet; + @Transient + private java.util.Queue queue; + @Transient + private java.util.Set set; + @Transient + private java.util.SortedSet sortedSet; + @Transient + private java.util.concurrent.TransferQueue transferQueue; + } + """ + ) + ); + } + + @Test + void handleInnerClasses() { + //language=java + rewriteRun( + java( + """ + import java.util.Collection; + import java.util.List; + + import javax.persistence.Entity; + import javax.persistence.Id; + + @Entity + public class UnannotatedCollectionEntity { + @Id + private int id; + + private Collection collectionField; + private List listField; + class InnerClass { + private Collection collectionField; + } + } + """, + """ + import java.util.Collection; + import java.util.List; + + import javax.persistence.Entity; + import javax.persistence.Id; + import javax.persistence.Transient; + + @Entity + public class UnannotatedCollectionEntity { + @Id + private int id; + + @Transient + private Collection collectionField; + @Transient + private List listField; + class InnerClass { + @Transient + private Collection collectionField; + } + } + """ + ) + ); + } +}