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, ExecutionContext> 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
+ * 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;
+ }
+ """
+ )
+ );
+ }
+}