diff --git a/build.gradle.kts b/build.gradle.kts index b22e6f3..4d8b5bd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -53,6 +53,7 @@ dependencies { testRuntimeOnly("jakarta.inject:jakarta.inject-api:2.0.1") testRuntimeOnly("jakarta.enterprise:jakarta.enterprise.cdi-api:4.1.0") testRuntimeOnly("org.projectlombok:lombok:latest.release") + testRuntimeOnly("jakarta.persistence:jakarta.persistence-api:3.2.0") } recipeDependencies { diff --git a/src/main/java/org/openrewrite/quarkus/RefactorTemporalAnnotation.java b/src/main/java/org/openrewrite/quarkus/RefactorTemporalAnnotation.java new file mode 100644 index 0000000..fd8d21a --- /dev/null +++ b/src/main/java/org/openrewrite/quarkus/RefactorTemporalAnnotation.java @@ -0,0 +1,133 @@ +/* + * Copyright 2025 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.quarkus; + +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; +import lombok.Value; +import org.jspecify.annotations.Nullable; +import org.openrewrite.*; +import org.openrewrite.java.ChangeType; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.RemoveAnnotation; +import org.openrewrite.java.TypeMatcher; +import org.openrewrite.java.search.FindAnnotations; +import org.openrewrite.java.search.UsesType; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.J.Annotation; +import org.openrewrite.java.tree.J.FieldAccess; +import org.openrewrite.java.tree.J.Identifier; + +@Value +@EqualsAndHashCode(callSuper = false) +public class RefactorTemporalAnnotation extends Recipe { + + private static final String TEMPORAL_ANNOTATION = "jakarta.persistence.Temporal"; + private static final String ENTITY_ANNOTATION = "jakarta.persistence.Entity"; + private static final String DATE_TYPE = "java.util.Date"; + private static final String JAVA_TIME_LOCAL_DATE = "java.time.LocalDate"; + private static final String JAVA_TIME_LOCAL_TIME = "java.time.LocalTime"; + private static final String JAVA_TIME_OFFSETDATETIME = "java.time.OffsetDateTime"; + private static final String JAVA_TIME_LOCAL_DATE_TIME = "java.time.LocalDateTime"; + + private static final TypeMatcher DATE_MATCHER = new TypeMatcher(DATE_TYPE); + + @Override + public String getDisplayName() { + return "Refactor `@Temporal` annotation `java.util.Date` fields to `java.time` API"; + } + + @Override + public String getDescription() { + return "Replace `java.util.Date` fields annotated with `@Temporal` " + + "with `java.time.LocalDate`, `java.time.LocalTime`, `java.time.LocalDateTime` or `java.time.OffsetDateTime`."; + } + + @Option(displayName = "Use offsetDateTime", + description = "If `true` the recipe will use `OffsetDateTime` instead of `LocalDateTime`. Default `false`.", + required = false) + @Nullable + Boolean useOffsetDateTime; + + @Override + public TreeVisitor getVisitor() { + return Preconditions.check( + Preconditions.and( + new UsesType<>(DATE_TYPE, true), + new UsesType<>(ENTITY_ANNOTATION, true), + new UsesType<>(TEMPORAL_ANNOTATION, true) + ), + new TemporalRefactorVisitor(Boolean.TRUE.equals(useOffsetDateTime)) + ); + } + + @RequiredArgsConstructor + private static class TemporalRefactorVisitor extends JavaIsoVisitor { + + private final boolean useOffsetDT; + + @Override + public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations multiVariable, ExecutionContext ctx) { + J.VariableDeclarations decls = super.visitVariableDeclarations(multiVariable, ctx); + + if (!DATE_MATCHER.matches(decls.getTypeExpression())) { + return decls; + } + + String newTypeToUse = FindAnnotations.find(decls, TEMPORAL_ANNOTATION) + .stream() + .findFirst() + .map(Annotation::getArguments) + .map(args -> args.get(0)) + .map(arg -> { + if (arg instanceof FieldAccess) { + return getNewType(((FieldAccess) arg).getSimpleName()); + } + if (arg instanceof Identifier) { + return getNewType(((Identifier) arg).getSimpleName()); + } + return null; + }) + .orElse(null); + if (newTypeToUse == null) { + return decls; + } + + doAfterVisit(new RemoveAnnotation(TEMPORAL_ANNOTATION).getVisitor()); + + maybeRemoveImport(DATE_TYPE); + maybeAddImport(newTypeToUse); + return (J.VariableDeclarations) new ChangeType(DATE_TYPE, newTypeToUse, null).getVisitor().visitNonNull(decls, ctx); + } + + private @Nullable String getNewType(String temporalConstant) { + switch (temporalConstant) { + case "DATE": + return JAVA_TIME_LOCAL_DATE; + case "TIME": + return JAVA_TIME_LOCAL_TIME; + case "TIMESTAMP": + if (useOffsetDT) { + return JAVA_TIME_OFFSETDATETIME; + } else { + return JAVA_TIME_LOCAL_DATE_TIME; + } + default: + return null; + } + } + } +} diff --git a/src/test/java/org/openrewrite/quarkus/RefactorTemporalAnnotationTest.java b/src/test/java/org/openrewrite/quarkus/RefactorTemporalAnnotationTest.java new file mode 100644 index 0000000..de8d940 --- /dev/null +++ b/src/test/java/org/openrewrite/quarkus/RefactorTemporalAnnotationTest.java @@ -0,0 +1,255 @@ +/* + * Copyright 2025 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.quarkus; + +import org.junit.jupiter.api.Test; +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 RefactorTemporalAnnotationTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipes(new RefactorTemporalAnnotation(null)) + .parser(JavaParser.fromJavaVersion() + .classpath("jakarta.persistence-api")); + } + + @DocumentExample + @Test + void shouldRemoveTemporalAnnotationAndKeepOtherAnnotations() { + rewriteRun( + //language=java + java( + """ + import jakarta.persistence.Column; + import jakarta.persistence.Entity; + import jakarta.persistence.Id; + import jakarta.persistence.Table; + import jakarta.persistence.Temporal; + + import java.util.Date; + + @Entity + @Table(name = "rent_house") + class RentHouseEntity { + @Id + @Column(name = "rent_house_id") + private Long id; + + @Column(name = "status") + private String status; + + @Column(name = "start_date") + @Temporal(TemporalType.DATE) + private Date startDate; + + @Column(name = "end_date") + @Temporal(TemporalType.DATE) + private Date endDate; + + @Column(name = "creation_date") + @Temporal(TemporalType.TIMESTAMP) + private Date creationDate; + } + """, + """ + import jakarta.persistence.Column; + import jakarta.persistence.Entity; + import jakarta.persistence.Id; + import jakarta.persistence.Table; + + import java.time.LocalDate; + import java.time.LocalDateTime; + + @Entity + @Table(name = "rent_house") + class RentHouseEntity { + @Id + @Column(name = "rent_house_id") + private Long id; + + @Column(name = "status") + private String status; + + @Column(name = "start_date") + private LocalDate startDate; + + @Column(name = "end_date") + private LocalDate endDate; + + @Column(name = "creation_date") + private LocalDateTime creationDate; + } + """ + ) + ); + } + + @Test + void shouldRemoveTemporalAnnotationAndUseCorrespondingType() { + rewriteRun( + //language=java + java( + """ + import java.util.Date; + import jakarta.persistence.Temporal; + import jakarta.persistence.TemporalType; + import jakarta.persistence.Entity; + import jakarta.persistence.Table; + + @Entity + @Table(name = "some_entity") + class MultiTemporalEntity { + @Temporal(TemporalType.DATE) + private Date dateField; + + @Temporal(TemporalType.TIMESTAMP) + private Date timestampField; + + @Temporal(TemporalType.TIME) + private Date timeField; + } + """, + """ + import jakarta.persistence.Entity; + import jakarta.persistence.Table; + + import java.time.LocalDate; + import java.time.LocalDateTime; + import java.time.LocalTime; + + @Entity + @Table(name = "some_entity") + class MultiTemporalEntity { + private LocalDate dateField; + + private LocalDateTime timestampField; + + private LocalTime timeField; + } + """ + ) + ); + } + + @Test + void shouldRemoveTemporalAnnotationAndUseLocalDateTimeTypeWhenStaticImport() { + rewriteRun( + //language=java + java( + """ + import java.util.Date; + import static jakarta.persistence.TemporalType.DATE; + import jakarta.persistence.Temporal; + import jakarta.persistence.Entity; + import jakarta.persistence.Table; + + @Entity + @Table(name = "some_entity") + class SomeEntity { + @Temporal(DATE) + private Date dateField; + } + """, + """ + import jakarta.persistence.Entity; + import jakarta.persistence.Table; + + import java.time.LocalDate; + + @Entity + @Table(name = "some_entity") + class SomeEntity { + private LocalDate dateField; + } + """ + ) + ); + } + + @Test + void shouldRemoveTemporalAnnotationWithOffsetDateTime() { + rewriteRun( + spec -> spec.recipe(new RefactorTemporalAnnotation(true)), + //language=java + java( + """ + import java.util.Date; + import jakarta.persistence.Temporal; + import jakarta.persistence.TemporalType; + import jakarta.persistence.Entity; + import jakarta.persistence.Table; + + @Entity + @Table(name = "some_entity") + class MultiTemporalEntity { + @Temporal(TemporalType.DATE) + private Date dateField; + + @Temporal(TemporalType.TIMESTAMP) + private Date timestampField; + + @Temporal(TemporalType.TIME) + private Date timeField; + } + """, + """ + import jakarta.persistence.Entity; + import jakarta.persistence.Table; + + import java.time.LocalDate; + import java.time.LocalTime; + import java.time.OffsetDateTime; + + @Entity + @Table(name = "some_entity") + class MultiTemporalEntity { + private LocalDate dateField; + + private OffsetDateTime timestampField; + + private LocalTime timeField; + } + """ + ) + ); + } + + @Test + void noAnnotationNoChange() { + rewriteRun( + //language=java + java( + """ + import jakarta.persistence.Entity; + import jakarta.persistence.Table; + import java.util.Date; + + @Entity + @Table(name = "some_entity") + class SomeEntity { + private Date createdOn; + } + """ + ) + ); + } +}