Skip to content

Commit 29599f9

Browse files
Keep jakarta.annotation-api dependency when moving to Jakarta with Spring Boot project and Jakarta annotations are used (#782)
* Keep `jakarta.annotation-api` dependency when moving to Jakarta with Spring Boot project and null annotations are used * Add license * Update src/test/java/org/openrewrite/java/migrate/jakarta/JavaxToJakartaTest.java Co-authored-by: Tim te Beek <[email protected]> * Implement multimodule solution * Stop looking through JavaProject after first annotation is found * Use `preVisit` to limit traversal, and find any Jakarta annotations --------- Co-authored-by: Tim te Beek <[email protected]>
1 parent a141c96 commit 29599f9

File tree

3 files changed

+336
-9
lines changed

3 files changed

+336
-9
lines changed
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
* <p>
4+
* Licensed under the Moderne Source Available License (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://docs.moderne.io/licensing/moderne-source-available-license
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite.java.migrate.jakarta;
17+
18+
import lombok.Value;
19+
import org.openrewrite.ExecutionContext;
20+
import org.openrewrite.ScanningRecipe;
21+
import org.openrewrite.Tree;
22+
import org.openrewrite.TreeVisitor;
23+
import org.openrewrite.java.marker.JavaProject;
24+
import org.openrewrite.java.search.FindAnnotations;
25+
import org.openrewrite.java.tree.J;
26+
import org.openrewrite.marker.SearchResult;
27+
28+
import java.util.HashSet;
29+
import java.util.Set;
30+
31+
public class HasNoJakartaAnnotations extends ScanningRecipe<HasNoJakartaAnnotations.Accumulator> {
32+
@Override
33+
public String getDisplayName() {
34+
return "Project has no Jakarta annotations";
35+
}
36+
37+
@Override
38+
public String getDescription() {
39+
return "Mark all source as found per `JavaProject` where no Jakarta annotations are found. " +
40+
"This is useful mostly as a precondition for recipes that require Jakarta annotations to be present";
41+
}
42+
43+
@Value
44+
public static class Accumulator {
45+
Set<JavaProject> projectsWithDependency;
46+
}
47+
48+
@Override
49+
public Accumulator getInitialValue(ExecutionContext ctx) {
50+
return new Accumulator(new HashSet<>());
51+
}
52+
53+
@Override
54+
public TreeVisitor<?, ExecutionContext> getScanner(HasNoJakartaAnnotations.Accumulator acc) {
55+
return new TreeVisitor<Tree, ExecutionContext>() {
56+
@Override
57+
public Tree preVisit(Tree tree, ExecutionContext ctx) {
58+
stopAfterPreVisit();
59+
if (tree instanceof J) {
60+
tree.getMarkers().findFirst(JavaProject.class)
61+
.filter(jp -> !acc.getProjectsWithDependency().contains(jp))
62+
.filter(jp -> !FindAnnotations.find((J) tree, "@jakarta.annotation.*", true).isEmpty())
63+
.ifPresent(jp -> acc.getProjectsWithDependency().add(jp));
64+
}
65+
return tree;
66+
}
67+
};
68+
}
69+
70+
@Override
71+
public TreeVisitor<?, ExecutionContext> getVisitor(HasNoJakartaAnnotations.Accumulator acc) {
72+
return new TreeVisitor<Tree, ExecutionContext>() {
73+
@Override
74+
public Tree preVisit(Tree tree, ExecutionContext ctx) {
75+
stopAfterPreVisit();
76+
return tree.getMarkers().findFirst(JavaProject.class)
77+
.filter(it -> !acc.getProjectsWithDependency().contains(it))
78+
.map(__ -> SearchResult.found(tree, "Project has no Jakarta annotations"))
79+
.orElse(tree);
80+
}
81+
};
82+
}
83+
}

src/main/resources/META-INF/rewrite/jakarta-ee-9.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,6 +1083,7 @@ name: org.openrewrite.java.migrate.jakarta.RemoveJakartaAnnotationDependency
10831083
displayName: Remove `jakarta.annotation-api` dependency when managed by Spring Boot
10841084
description: Counteract the `jakarta.annotation-api` added by `org.openrewrite.java.migrate.javax.AddCommonAnnotationsDependencies` for Spring Boot applications.
10851085
preconditions:
1086+
- org.openrewrite.java.migrate.jakarta.HasNoJakartaAnnotations
10861087
- org.openrewrite.java.dependencies.DependencyInsight:
10871088
groupIdPattern: org.springframework.boot
10881089
artifactIdPattern: spring-boot-starter

src/test/java/org/openrewrite/java/migrate/jakarta/JavaxToJakartaTest.java

Lines changed: 252 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
import org.openrewrite.test.RecipeSpec;
2525
import org.openrewrite.test.RewriteTest;
2626

27+
import static org.openrewrite.gradle.Assertions.buildGradle;
28+
import static org.openrewrite.gradle.Assertions.settingsGradle;
29+
import static org.openrewrite.gradle.toolingapi.Assertions.withToolingApi;
2730
import static org.openrewrite.java.Assertions.*;
2831
import static org.openrewrite.maven.Assertions.pomXml;
2932
import static org.openrewrite.xml.Assertions.xml;
@@ -60,6 +63,16 @@ public void foo() {}
6063
}
6164
""";
6265

66+
@Language("java")
67+
private static final String jakartaAnnotation =
68+
"""
69+
package jakarta.annotation;
70+
public @interface Nonnull {
71+
}
72+
public @interface Nullable {
73+
}
74+
""";
75+
6376
@Override
6477
public void defaults(RecipeSpec spec) {
6578
spec.recipe(
@@ -442,9 +455,7 @@ void projectWithSpringBootStarterWeb() {
442455
//language=xml
443456
pomXml(
444457
"""
445-
<?xml version="1.0" encoding="UTF-8"?>
446-
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
447-
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
458+
<project>
448459
<modelVersion>4.0.0</modelVersion>
449460
<parent>
450461
<groupId>org.springframework.boot</groupId>
@@ -511,9 +522,7 @@ void projectWithSpringBoot3StarterWebShouldRemoveJakartaDependency() {
511522
//language=xml
512523
pomXml(
513524
"""
514-
<?xml version="1.0" encoding="UTF-8"?>
515-
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
516-
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
525+
<project>
517526
<modelVersion>4.0.0</modelVersion>
518527
<parent>
519528
<groupId>org.springframework.boot</groupId>
@@ -538,9 +547,7 @@ void projectWithSpringBoot3StarterWebShouldRemoveJakartaDependency() {
538547
</project>
539548
""",
540549
"""
541-
<?xml version="1.0" encoding="UTF-8"?>
542-
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
543-
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
550+
<project>
544551
<modelVersion>4.0.0</modelVersion>
545552
<parent>
546553
<groupId>org.springframework.boot</groupId>
@@ -574,6 +581,242 @@ public class TestApplication {
574581
);
575582
}
576583

584+
@Test
585+
void projectWithSpringBoot3StarterWebShouldNotRemoveJakartaDependencyWhenUsingNonnullAnnotation() {
586+
rewriteRun(
587+
spec -> spec.parser(JavaParser.fromJavaVersion().dependsOn(javaxServlet, jakartaAnnotation)),
588+
mavenProject(
589+
"Sample",
590+
//language=xml
591+
pomXml(
592+
"""
593+
<project>
594+
<modelVersion>4.0.0</modelVersion>
595+
<parent>
596+
<groupId>org.springframework.boot</groupId>
597+
<artifactId>spring-boot-starter-parent</artifactId>
598+
<version>3.2.5</version>
599+
<relativePath/> <!-- lookup parent from repository -->
600+
</parent>
601+
<groupId>com.example</groupId>
602+
<artifactId>demo</artifactId>
603+
<version>0.0.1-SNAPSHOT</version>
604+
<dependencies>
605+
<dependency>
606+
<groupId>jakarta.annotation</groupId>
607+
<artifactId>jakarta.annotation-api</artifactId>
608+
<version>1.3.5</version>
609+
</dependency>
610+
<dependency>
611+
<groupId>org.springframework.boot</groupId>
612+
<artifactId>spring-boot-starter-web</artifactId>
613+
</dependency>
614+
</dependencies>
615+
</project>
616+
""",
617+
"""
618+
<project>
619+
<modelVersion>4.0.0</modelVersion>
620+
<parent>
621+
<groupId>org.springframework.boot</groupId>
622+
<artifactId>spring-boot-starter-parent</artifactId>
623+
<version>3.2.5</version>
624+
<relativePath/> <!-- lookup parent from repository -->
625+
</parent>
626+
<groupId>com.example</groupId>
627+
<artifactId>demo</artifactId>
628+
<version>0.0.1-SNAPSHOT</version>
629+
<dependencies>
630+
<dependency>
631+
<groupId>jakarta.annotation</groupId>
632+
<artifactId>jakarta.annotation-api</artifactId>
633+
<version>2.0.0</version>
634+
</dependency>
635+
<dependency>
636+
<groupId>org.springframework.boot</groupId>
637+
<artifactId>spring-boot-starter-web</artifactId>
638+
</dependency>
639+
</dependencies>
640+
</project>
641+
"""
642+
),
643+
srcMainJava(
644+
//language=java
645+
java(
646+
"""
647+
import jakarta.annotation.Nonnull;
648+
649+
public class TestApplication {
650+
@Nonnull
651+
public String upperCase(@Nonnull String input) {
652+
return input.toUpperCase();
653+
}
654+
}
655+
"""
656+
)
657+
)
658+
)
659+
);
660+
}
661+
662+
@Test
663+
void projectWithSpringBoot3StarterWebShouldNotRemoveJakartaDependencyWhenUsingNullableAnnotation() {
664+
rewriteRun(
665+
spec -> spec.parser(JavaParser.fromJavaVersion().dependsOn(javaxServlet, jakartaAnnotation)),
666+
mavenProject(
667+
"Sample",
668+
//language=xml
669+
pomXml(
670+
"""
671+
<project>
672+
<modelVersion>4.0.0</modelVersion>
673+
<parent>
674+
<groupId>org.springframework.boot</groupId>
675+
<artifactId>spring-boot-starter-parent</artifactId>
676+
<version>3.2.5</version>
677+
<relativePath/> <!-- lookup parent from repository -->
678+
</parent>
679+
<groupId>com.example</groupId>
680+
<artifactId>demo</artifactId>
681+
<version>0.0.1-SNAPSHOT</version>
682+
<dependencies>
683+
<dependency>
684+
<groupId>jakarta.annotation</groupId>
685+
<artifactId>jakarta.annotation-api</artifactId>
686+
<version>1.3.5</version>
687+
</dependency>
688+
<dependency>
689+
<groupId>org.springframework.boot</groupId>
690+
<artifactId>spring-boot-starter-web</artifactId>
691+
</dependency>
692+
</dependencies>
693+
</project>
694+
""",
695+
"""
696+
<project>
697+
<modelVersion>4.0.0</modelVersion>
698+
<parent>
699+
<groupId>org.springframework.boot</groupId>
700+
<artifactId>spring-boot-starter-parent</artifactId>
701+
<version>3.2.5</version>
702+
<relativePath/> <!-- lookup parent from repository -->
703+
</parent>
704+
<groupId>com.example</groupId>
705+
<artifactId>demo</artifactId>
706+
<version>0.0.1-SNAPSHOT</version>
707+
<dependencies>
708+
<dependency>
709+
<groupId>jakarta.annotation</groupId>
710+
<artifactId>jakarta.annotation-api</artifactId>
711+
<version>2.0.0</version>
712+
</dependency>
713+
<dependency>
714+
<groupId>org.springframework.boot</groupId>
715+
<artifactId>spring-boot-starter-web</artifactId>
716+
</dependency>
717+
</dependencies>
718+
</project>
719+
"""
720+
),
721+
srcMainJava(
722+
//language=java
723+
java(
724+
"""
725+
import jakarta.annotation.Nullable;
726+
727+
public class TestApplication {
728+
@Nullable
729+
public String safeUpperCase(@Nullable String input) {
730+
return input == null ? null : input.toUpperCase();
731+
}
732+
}
733+
"""
734+
)
735+
)
736+
)
737+
);
738+
}
739+
740+
@Test
741+
void multiProjectWithSpringBoot3StarterWebShouldRemoveJakartaDependencyWhenUsingNullableAnnotationWhenApplicable() {
742+
rewriteRun(
743+
spec -> spec.beforeRecipe(withToolingApi()).parser(JavaParser.fromJavaVersion().dependsOn(javaxServlet, jakartaAnnotation)),
744+
mavenProject("multi-project-build",
745+
//language=groovy
746+
settingsGradle("""
747+
include 'project-with-null-annotations'
748+
include 'project-without-null-annotations'
749+
"""),
750+
mavenProject("project-with-null-annotations",
751+
//language=groovy
752+
buildGradle(
753+
"""
754+
plugins {
755+
id 'java'
756+
}
757+
758+
repositories {
759+
mavenCentral()
760+
}
761+
762+
dependencies {
763+
implementation 'jakarta.annotation:jakarta.annotation-api:1.3.5'
764+
implementation 'org.springframework.boot:spring-boot-starter-web'
765+
}
766+
""",
767+
"""
768+
plugins {
769+
id 'java'
770+
}
771+
772+
repositories {
773+
mavenCentral()
774+
}
775+
776+
dependencies {
777+
implementation 'jakarta.annotation:jakarta.annotation-api:2.0.0'
778+
implementation 'org.springframework.boot:spring-boot-starter-web'
779+
}
780+
"""
781+
),
782+
srcMainJava(
783+
//language=java
784+
java(
785+
"""
786+
import jakarta.annotation.Nullable;
787+
788+
public class TestApplication {
789+
@Nullable
790+
public String safeUpperCase(@Nullable String input) {
791+
return input == null ? null : input.toUpperCase();
792+
}
793+
}
794+
"""
795+
)
796+
)
797+
),
798+
mavenProject("project-without-null-annotations",
799+
//language=groovy
800+
buildGradle(
801+
"""
802+
plugins {
803+
id 'java'
804+
}
805+
806+
repositories {
807+
mavenCentral()
808+
}
809+
810+
dependencies {
811+
implementation 'org.springframework.boot:spring-boot-starter-web'
812+
}
813+
"""
814+
)
815+
)
816+
)
817+
);
818+
}
819+
577820
@Test
578821
void upgradeAnnotationApiFromV1ToV2() {
579822
rewriteRun(

0 commit comments

Comments
 (0)