Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
142 changes: 142 additions & 0 deletions src/main/java/org/openrewrite/quarkus/RefactorTemporalAnnotation.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* Copyright 2025 the original author or authors.
* <p>
* 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
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.Value;
import org.jetbrains.annotations.NotNull;
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;

import java.util.Objects;

@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";

@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(
new UsesType<>(ENTITY_ANNOTATION, true),
new TemporalRefactorVisitor(useOffsetDateTime)
);
}

private static class TemporalRefactorVisitor extends JavaIsoVisitor<ExecutionContext> {

private static final TypeMatcher DATE_MATCHER = new TypeMatcher(DATE_TYPE);

private final Boolean useOffsetDT;

public TemporalRefactorVisitor(Boolean useOffsetDateTime) {
useOffsetDT = useOffsetDateTime;
}

@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;
}

Annotation temporalAnnotation = FindAnnotations.find(decls, TEMPORAL_ANNOTATION).stream()
.findFirst()
.orElse(null);

if (temporalAnnotation == null) {
return decls;
}

// Extract the enum constant (DATE or TIMESTAMP)
String newTypeToUse = Objects.requireNonNull(temporalAnnotation.getArguments()).stream()
.findFirst()
.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());
decls = (J.VariableDeclarations) new ChangeType(DATE_TYPE, newTypeToUse, null).getVisitor().visitNonNull(decls, ctx);

maybeRemoveImport(DATE_TYPE);
maybeAddImport(newTypeToUse);
return decls;
}

private String getNewType(String temporalConstant) {
switch (temporalConstant) {
case "DATE":
return JAVA_TIME_LOCAL_DATE;
case "TIME":
return JAVA_TIME_LOCAL_TIME;
case "TIMESTAMP":
if (Boolean.TRUE.equals(useOffsetDT)) {
return JAVA_TIME_OFFSETDATETIME;
} else {
return JAVA_TIME_LOCAL_DATE_TIME;
}
default:
return null;
}
}
}
}
Loading