diff --git a/components/jaxrs-recipes/pom.xml b/components/jaxrs-recipes/pom.xml
new file mode 100644
index 000000000..adaabb971
--- /dev/null
+++ b/components/jaxrs-recipes/pom.xml
@@ -0,0 +1,240 @@
+
+
+
+
+ 4.0.0
+
+ org.springframework.sbm
+ jaxrs-recipes
+ 0.15.2-SNAPSHOT
+ jar
+
+
+ UTF-8
+ UTF-8
+ 2.5.0
+ 17
+ 17
+ 3.11.0
+ 17
+ 2.1.14
+ 8.29.0
+ 4.32.0
+ 1.17.0
+ 3.3.1
+ 0.10.0
+ 1.19.1
+ 3.2.0
+ 3.1.4
+ 1.18.30
+ UTF-8
+ UTF-8
+ src/generated/java
+ 1.33
+ 0.0.7
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-dependencies
+ ${spring-boot.version}
+ pom
+ import
+
+
+ org.openrewrite.recipe
+ rewrite-recipe-bom
+ 2.14.0
+ pom
+ import
+
+
+
+
+
+
+
+ org.openrewrite.maven
+ rewrite-maven-plugin
+ 5.20.0
+
+
+
+
+ org.openrewrite
+ rewrite-core
+
+
+ org.openrewrite
+ rewrite-java
+
+
+
+ org.springframework.rewrite
+ spring-rewrite-commons-plugin-invoker-polyglot
+ 0.1.0-SNAPSHOT
+ test
+
+
+
+
+ org.springframework.sbm
+ sbm-core
+ ${project.version}
+
+
+ org.openrewrite
+ rewrite-java
+
+
+ org.openrewrite
+ rewrite-core
+
+
+
+
+
+ com.tngtech.archunit
+ archunit-junit5
+ 1.2.0
+
+
+ org.springframework.sbm
+ sbm-support-jee
+ 0.15.2-SNAPSHOT
+
+
+ org.springframework.sbm
+ sbm-core
+
+
+
+
+
+
+ org.aspectj
+ aspectjweaver
+ 1.9.7
+
+
+
+
+ javax.xml.bind
+ jaxb-api
+ 2.3.1
+
+
+ org.glassfish.jaxb
+ jaxb-runtime
+ 2.3.3
+
+
+
+ org.springframework.sbm
+ test-helper
+ ${project.version}
+ test
+
+
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.sbm
+ recipe-test-support
+ ${project.version}
+ test
+
+
+ org.springframework.sbm
+ sbm-core
+
+
+
+
+ org.springframework.sbm
+ sbm-core
+ 0.15.2-SNAPSHOT
+ tests
+ test
+
+
+ org.openrewrite
+ rewrite-core
+ 8.29.0
+ compile
+
+
+ org.openrewrite.recipe
+ rewrite-static-analysis
+ 1.11.0
+ compile
+
+
+ org.openrewrite
+ rewrite-test
+ 8.29.0
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.1.2
+
+ integration
+ methods
+ 10
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+ 3.1.2
+
+ integration
+
+ **/*Test.java
+
+ false
+
+
+
+
+ integration-test
+ verify
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/components/jaxrs-recipes/src/main/java/example/recipe/SbmAdapterRecipe.java b/components/jaxrs-recipes/src/main/java/example/recipe/SbmAdapterRecipe.java
new file mode 100644
index 000000000..c1543ac77
--- /dev/null
+++ b/components/jaxrs-recipes/src/main/java/example/recipe/SbmAdapterRecipe.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2021 - 2023 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 example.recipe;
+
+
+import freemarker.template.Configuration;
+import org.openrewrite.*;
+import org.openrewrite.internal.lang.Nullable;
+import org.openrewrite.maven.MavenSettings;
+import org.openrewrite.maven.cache.LocalMavenArtifactCache;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.rewrite.parser.JavaParserBuilder;
+import org.springframework.rewrite.parser.maven.MavenSettingsInitializer;
+import org.springframework.rewrite.parser.maven.RewriteMavenArtifactDownloader;
+import org.springframework.rewrite.resource.*;
+import org.springframework.rewrite.scopes.ProjectMetadata;
+import org.springframework.sbm.build.impl.MavenBuildFileRefactoringFactory;
+import org.springframework.sbm.build.impl.RewriteMavenParser;
+import org.springframework.sbm.build.resource.BuildFileResourceWrapper;
+import org.springframework.sbm.engine.context.ProjectContext;
+import org.springframework.sbm.engine.context.ProjectContextFactory;
+import org.springframework.sbm.engine.recipe.RewriteRecipeLoader;
+import org.springframework.sbm.java.JavaSourceProjectResourceWrapper;
+import org.springframework.sbm.java.refactoring.JavaRefactoringFactory;
+import org.springframework.sbm.java.refactoring.JavaRefactoringFactoryImpl;
+import org.springframework.sbm.java.util.BasePackageCalculator;
+import org.springframework.sbm.jee.jaxrs.MigrateJaxRsRecipe;
+import org.springframework.sbm.jee.jaxrs.actions.ConvertJaxRsAnnotations;
+import org.springframework.sbm.project.resource.ProjectResourceSetHolder;
+import org.springframework.sbm.project.resource.ProjectResourceWrapper;
+import org.springframework.sbm.project.resource.ProjectResourceWrapperRegistry;
+import org.springframework.sbm.project.resource.SbmApplicationProperties;
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * @author Fabian Krüger
+ */
+public class SbmAdapterRecipe extends ScanningRecipe> {
+
+ private ProjectResourceSetFactory projectResourceSetFactory;
+ private ProjectContextFactory projectContextFactory;
+
+ @Override
+ public @NlsRewrite.DisplayName String getDisplayName() {
+ return "";
+ }
+
+ @Override
+ public @NlsRewrite.Description String getDescription() {
+ return "";
+ }
+
+ public SbmAdapterRecipe() {
+
+ }
+
+ @Override
+ public List getInitialValue(ExecutionContext ctx) {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public TreeVisitor, ExecutionContext> getScanner(List acc) {
+ return new TreeVisitor() {
+ @Override
+ public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext executionContext) {
+ if(tree instanceof SourceFile) {
+ acc.add((SourceFile) tree);
+ }
+ return super.visit(tree, executionContext);
+ }
+ };
+ }
+
+ @Override
+ public Collection extends SourceFile> generate(List generate, ExecutionContext executionContext) {
+ // create the required classes
+ initBeans();
+
+ // transform nodes to SourceFiles
+ List sourceFiles = generate;
+
+ // FIXME: base dir calculation is fake
+ Path baseDir = Path.of(".").resolve("testcode/jee/jaxrs/bootify-jaxrs/given").toAbsolutePath().normalize(); //executionContext.getMessage("base.dir");
+
+ // Create the SBM resource set abstraction
+ ProjectResourceSet projectResourceSet = projectResourceSetFactory.create(baseDir, sourceFiles);
+ // Create the SBM ProjectContext
+ ProjectContext pc = projectContextFactory.createProjectContext(baseDir, projectResourceSet);
+
+ // Execute the SBM Action = the JAXRS Recipe
+// new ConvertJaxRsAnnotations().apply(pc);
+
+ RewriteRecipeLoader recipeLoader = new RewriteRecipeLoader();
+ new MigrateJaxRsRecipe().jaxRs(recipeLoader).apply(pc);
+
+ List extends SourceFile> list = pc.getProjectResources().stream()
+ .map(pr -> pr.getSourceFile())
+ .toList();
+
+ return list;
+ }
+
+ private void initBeans() {
+ // ExecutionContext is not retrieved from OpenRewrite here
+ AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext("org.springframework.freemarker", "org.springframework.sbm", "org.springframework.rewrite");
+ ctx.register(Configuration.class);
+ this.projectContextFactory = ctx.getBean(ProjectContextFactory.class);
+ this.projectResourceSetFactory = ctx.getBean(ProjectResourceSetFactory.class);
+ }
+
+}
diff --git a/components/jaxrs-recipes/src/main/java/org/springframework/sbm/FreemarkerConfiguration.java b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/FreemarkerConfiguration.java
new file mode 100644
index 000000000..c5f2dedae
--- /dev/null
+++ b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/FreemarkerConfiguration.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2021 - 2023 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.springframework.sbm;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author Fabian Krüger
+ */
+@Configuration
+public class FreemarkerConfiguration {
+ @Bean
+ @ConditionalOnMissingBean(type = "freemarker.template.Configuration")
+ public freemarker.template.Configuration configuration() {
+ return new freemarker.template.Configuration();
+ }
+}
diff --git a/components/jaxrs-recipes/src/main/java/org/springframework/sbm/UserInteractionsDummy.java b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/UserInteractionsDummy.java
new file mode 100644
index 000000000..d604ac178
--- /dev/null
+++ b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/UserInteractionsDummy.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2021 - 2023 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.springframework.sbm;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.sbm.engine.recipe.UserInteractions;
+import org.springframework.stereotype.Component;
+
+@Component
+@ConditionalOnMissingBean(type = "org.springframework.sbm.UserInteractions")
+public class UserInteractionsDummy implements UserInteractions {
+ @Override
+ public boolean askUserYesOrNo(String question) {
+ return true;
+ }
+
+ @Override
+ public String askForInput(String question) {
+ return "answer";
+ }
+}
diff --git a/components/jaxrs-recipes/src/main/java/org/springframework/sbm/java/migration/conditions/HasTypeAnnotation.java b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/java/migration/conditions/HasTypeAnnotation.java
new file mode 100644
index 000000000..408b95548
--- /dev/null
+++ b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/java/migration/conditions/HasTypeAnnotation.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2021 - 2023 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.springframework.sbm.java.migration.conditions;
+
+import lombok.*;
+import org.springframework.sbm.engine.context.ProjectContext;
+import org.springframework.sbm.engine.recipe.Condition;
+
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class HasTypeAnnotation implements Condition {
+
+ @Setter
+ @Getter
+ private String annotation;
+
+ @Override
+ public String getDescription() {
+ return "If there are any types annotated with " + annotation;
+ }
+
+ @Override
+ public boolean evaluate(ProjectContext context) {
+ return context.getProjectJavaSources().asStream()
+ .flatMap(js -> js.getTypes().stream())
+ .anyMatch(t -> t.hasAnnotation(annotation));
+ }
+
+}
diff --git a/components/jaxrs-recipes/src/main/java/org/springframework/sbm/java/migration/recipes/RewriteMethodInvocation.java b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/java/migration/recipes/RewriteMethodInvocation.java
new file mode 100644
index 000000000..d8007be3c
--- /dev/null
+++ b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/java/migration/recipes/RewriteMethodInvocation.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2021 - 2023 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.springframework.sbm.java.migration.recipes;
+
+import lombok.RequiredArgsConstructor;
+import org.openrewrite.ExecutionContext;
+import org.openrewrite.NlsRewrite;
+import org.openrewrite.Recipe;
+import org.openrewrite.TreeVisitor;
+import org.openrewrite.java.AddImport;
+import org.openrewrite.java.JavaVisitor;
+import org.openrewrite.java.MethodMatcher;
+import org.openrewrite.java.tree.J;
+import org.openrewrite.java.tree.J.MethodInvocation;
+import org.openrewrite.java.tree.JavaType;
+import org.openrewrite.java.tree.TypeUtils;
+
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+@RequiredArgsConstructor
+public class RewriteMethodInvocation extends Recipe {
+
+ final private Predicate checkMethodInvocation;
+ final private Transformer transformer;
+
+ @Override
+ public String getDisplayName() {
+ return "Rewrite method invocation";
+ }
+
+ @Override
+ public @NlsRewrite.Description String getDescription() {
+ return getDisplayName();
+ }
+
+ @Override
+ public TreeVisitor, ExecutionContext> getVisitor() {
+ return new JavaVisitor() {
+
+ @Override
+ public J visitMethodInvocation(MethodInvocation method, ExecutionContext p) {
+ MethodInvocation m = (MethodInvocation) super.visitMethodInvocation(method, p);
+ if (checkMethodInvocation.test(m)) {
+ return transformer.transform(this, m, this::addImport);
+ }
+ return m;
+ }
+
+ private void addImport(String fqName) {
+ AddImport op = new AddImport<>(fqName, null, false);
+ if (!getAfterVisit().contains(op)) {
+ doAfterVisit(op);
+ }
+ }
+ };
+ }
+
+ public static Predicate methodInvocationMatcher(String signature) {
+ MethodMatcher methodMatcher = new MethodMatcher(signature);
+ return mi -> methodMatcher.matches(mi);
+ }
+
+ public interface Transformer {
+
+ J transform(JavaVisitor visitor, MethodInvocation currentInvocation, Consumer addImport);
+
+ }
+
+ public static RewriteMethodInvocation renameMethodInvocation(Predicate matcher, String newName, String newType) {
+ JavaType type = JavaType.buildType(newType);
+ return new RewriteMethodInvocation(matcher, (v, m, a) -> {
+ return m.withName(m.getName().withSimpleName(newName))
+ .withMethodType(m.getMethodType().withReturnType(type))
+ .withDeclaringType(TypeUtils.asFullyQualified(type));
+ });
+ }
+
+}
diff --git a/components/jaxrs-recipes/src/main/java/org/springframework/sbm/java/migration/recipes/openrewrite/ReplaceConstantWithAnotherConstant.java b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/java/migration/recipes/openrewrite/ReplaceConstantWithAnotherConstant.java
new file mode 100644
index 000000000..f35ee7045
--- /dev/null
+++ b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/java/migration/recipes/openrewrite/ReplaceConstantWithAnotherConstant.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2021 - 2023 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.springframework.sbm.java.migration.recipes.openrewrite;
+
+import lombok.EqualsAndHashCode;
+import lombok.Value;
+import org.openrewrite.*;
+import org.openrewrite.internal.lang.Nullable;
+import org.openrewrite.java.JavaTemplate;
+import org.openrewrite.java.JavaVisitor;
+import org.openrewrite.java.search.UsesType;
+import org.openrewrite.java.tree.J;
+import org.openrewrite.java.tree.JavaType;
+import org.openrewrite.java.tree.TypeUtils;
+
+@Value
+@EqualsAndHashCode(callSuper = false)
+// TODO: Remove this class and use the one in Openrewrite core once we use 7.39.1 or later
+public class ReplaceConstantWithAnotherConstant extends Recipe {
+
+ @Option(displayName = "Fully qualified name of the constant to replace", example = "org.springframework.http.MediaType.APPLICATION_JSON_VALUE")
+ String existingFullyQualifiedConstantName;
+
+ @Option(displayName = "Fully qualified name of the constant to use in place of existing constant", example = "org.springframework.http.MediaType.APPLICATION_JSON_VALUE")
+ String fullyQualifiedConstantName;
+
+ @Override
+ public String getDisplayName() {
+ return "Replace constant with another constant";
+ }
+
+ @Override
+ public String getDescription() {
+ return "Replace constant with another constant, adding/removing import on class if needed.";
+ }
+
+
+
+ // FIXME: (jaxrs) Call from visitor to decide if applicable
+ protected TreeVisitor, ExecutionContext> getSingleSourceApplicableTest() {
+ return new UsesType<>(existingFullyQualifiedConstantName.substring(0,existingFullyQualifiedConstantName.lastIndexOf('.')), true);
+ }
+
+ @Override
+ public JavaVisitor getVisitor() {
+ return new ReplaceConstantWithAnotherConstantVisitor(existingFullyQualifiedConstantName, fullyQualifiedConstantName);
+ }
+
+ private static class ReplaceConstantWithAnotherConstantVisitor extends JavaVisitor {
+
+ private final String existingOwningType;
+ private final String constantName;
+ private final String owningType;
+ private final String template;
+
+ public ReplaceConstantWithAnotherConstantVisitor(String existingFullyQualifiedConstantName, String fullyQualifiedConstantName) {
+ this.existingOwningType = existingFullyQualifiedConstantName.substring(0, existingFullyQualifiedConstantName.lastIndexOf('.'));
+ this.constantName = existingFullyQualifiedConstantName.substring(existingFullyQualifiedConstantName.lastIndexOf('.') + 1);
+ this.owningType = fullyQualifiedConstantName.substring(0, fullyQualifiedConstantName.lastIndexOf('.'));
+ this.template = fullyQualifiedConstantName.substring(owningType.lastIndexOf('.') + 1);
+ }
+
+ @Override
+ public J visitFieldAccess(J.FieldAccess fieldAccess, ExecutionContext executionContext) {
+ if (isConstant(fieldAccess.getName().getFieldType())) {
+ maybeRemoveImport(existingOwningType);
+ if (existingOwningType.contains("$")) {
+ maybeRemoveImport(existingOwningType.substring(0, existingOwningType.indexOf('$')));
+ }
+ maybeAddImport(owningType, false);
+
+ fieldAccess = JavaTemplate.builder(template).imports(owningType).build()
+ .apply(getCursor(), fieldAccess.getCoordinates().replace())
+ .withPrefix(fieldAccess.getPrefix());
+
+ return fieldAccess;
+ }
+ return super.visitFieldAccess(fieldAccess, executionContext);
+ }
+
+ @Override
+ public J visitIdentifier(J.Identifier ident, ExecutionContext executionContext) {
+ if (isConstant(ident.getFieldType()) && !isVariableDeclaration()) {
+ maybeRemoveImport(existingOwningType);
+ maybeAddImport(owningType, false);
+
+ return JavaTemplate.builder(template)
+ .imports(owningType)
+ .build()
+ .apply(getCursor(), ident.getCoordinates().replace())
+ .withPrefix(ident.getPrefix());
+ }
+ return super.visitIdentifier(ident, executionContext);
+ }
+
+ private boolean isConstant(@Nullable JavaType.Variable varType) {
+ return varType != null && TypeUtils.isOfClassType(varType.getOwner(), existingOwningType) &&
+ varType.getName().equals(constantName);
+ }
+
+ private boolean isVariableDeclaration() {
+ Cursor maybeVariable = getCursor().dropParentUntil(is -> is instanceof J.VariableDeclarations || is instanceof J.CompilationUnit);
+ if (!(maybeVariable.getValue() instanceof J.VariableDeclarations)) {
+ return false;
+ }
+ JavaType.Variable variableType = ((J.VariableDeclarations) maybeVariable.getValue()).getVariables().get(0).getVariableType();
+ if (variableType == null) {
+ return true;
+ }
+
+ JavaType.FullyQualified ownerFqn = TypeUtils.asFullyQualified(variableType.getOwner());
+ if (ownerFqn == null) {
+ return true;
+ }
+
+ return constantName.equals(((J.VariableDeclarations) maybeVariable.getValue()).getVariables().get(0).getSimpleName()) &&
+ existingOwningType.equals(ownerFqn.getFullyQualifiedName());
+ }
+ }
+}
diff --git a/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/ejb/actions/MigrateEjbAnnotations.java b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/ejb/actions/MigrateEjbAnnotations.java
new file mode 100644
index 000000000..9842ca1e1
--- /dev/null
+++ b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/ejb/actions/MigrateEjbAnnotations.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2021 - 2023 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.springframework.sbm.jee.ejb.actions;
+
+import org.springframework.sbm.engine.recipe.AbstractAction;
+import org.springframework.sbm.java.api.*;
+import org.springframework.sbm.engine.context.ProjectContext;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Migrates {@code @EJB} annotations to {@code @Autowired}.
+ */
+public class MigrateEjbAnnotations extends AbstractAction {
+
+ public static final String EJB_ANNOTATION = "javax.ejb.EJB";
+ private static final String AUTOWIRED_ANNOTATION = "org.springframework.beans.factory.annotation.Autowired";
+ private static final String QUALIFIER_ANNOTATION = "org.springframework.beans.factory.annotation.Qualifier";
+
+ @Override
+ public void apply(ProjectContext context) {
+ context.getProjectJavaSources().list().stream()
+ .map(JavaSource::getTypes)
+ .flatMap(List::stream)
+ .forEach(this::migrateEJBAnnotations);
+ }
+
+ private void migrateEJBAnnotations(Type type) {
+
+ // if a setter is annotated migrate it and annotate the matching member
+ List fieldsWithMethodInjection = new ArrayList<>();
+ type.getMethods().stream()
+ .filter(method -> method.getName().startsWith("set"))
+ .filter(method -> method.hasAnnotation(EJB_ANNOTATION))
+ .forEach(method -> {
+ String affectedMemberName = this.migrateEjbAnnotation(method);
+ fieldsWithMethodInjection.add(affectedMemberName);
+ });
+
+ type.getMembers().stream()
+ .filter(member -> member.hasAnnotation(EJB_ANNOTATION))
+ .filter(member -> !fieldsWithMethodInjection.contains(member.getName()))
+ .forEach(member -> this.migrateEjbAnnotation(member));
+
+ }
+
+ private String migrateEjbAnnotation(Method method) {
+ Annotation annotation = method.getAnnotation(EJB_ANNOTATION).get();
+
+ String autowiredAnnotationString = renderAutowiredAnnotation(annotation);
+ method.removeAnnotation(annotation);
+ method.addAnnotation(autowiredAnnotationString, AUTOWIRED_ANNOTATION);
+
+ Optional qualifierAnnotation = buildQualifierAnnotation(annotation);
+ if (qualifierAnnotation.isPresent()) {
+ method.addAnnotation(qualifierAnnotation.get(), QUALIFIER_ANNOTATION);
+ }
+ String memberName = method.getName().substring(3);
+ memberName = Character.toLowerCase(memberName.charAt(0)) + memberName.substring(1);
+ return memberName;
+ }
+
+ private void migrateEjbAnnotation(Member member) {
+ Annotation annotation = member.getAnnotation(EJB_ANNOTATION);
+
+ String autowiredAnnotationString = renderAutowiredAnnotation(annotation);
+ member.removeAnnotation(annotation);
+ member.addAnnotation(autowiredAnnotationString, AUTOWIRED_ANNOTATION);
+
+ Optional qualifierAnnotation = buildQualifierAnnotation(annotation);
+ if (qualifierAnnotation.isPresent()) {
+ member.addAnnotation(qualifierAnnotation.get(), QUALIFIER_ANNOTATION);
+ }
+ }
+
+ private Optional buildQualifierAnnotation(Annotation annotation) {
+ if (annotation.getAttribute("beanName") != null) {
+ StringBuilder stringBuilder = new StringBuilder();
+ String beanName = annotation.getAttribute("beanName").getAssignmentRightSide().printVariable();
+ stringBuilder.append("@Qualifier(").append("\"").append(beanName).append("\"").append(")");
+ return Optional.of(stringBuilder.toString());
+ }
+ return Optional.empty();
+ }
+
+ private String renderAutowiredAnnotation(Annotation annotation) {
+ StringBuilder autowiredSb = new StringBuilder();
+ Optional comment = renderComment(annotation);
+
+ if (comment.isPresent()) {
+ autowiredSb.append(comment.get());
+ }
+ autowiredSb.append("@Autowired");
+ return autowiredSb.toString();
+ }
+
+ private Optional renderComment(Annotation annotation) {
+ List commentLines = new ArrayList<>();
+ StringBuilder autowiredSb = new StringBuilder();
+
+ if (annotation.getAttribute("description") != null) {
+ String description = annotation.getAttribute("description").getAssignmentRightSide().printVariable();
+ commentLines.add(description.trim());
+ }
+ if (annotation.getAttribute("lookup") != null) {
+ String lookup = annotation.getAttribute("lookup").getAssignmentRightSide().printVariable();
+ commentLines.add("SBM-TODO: lookup was '" + lookup.trim() + "'");
+ }
+ if (annotation.getAttribute("beanInterface") != null) {
+ String beanInterface = annotation.getAttribute("beanInterface").getAssignmentRightSide().printVariable();
+ commentLines.add("SBM-TODO: beanInterface was '" + beanInterface.trim() + "'");
+ }
+ if (!commentLines.isEmpty()) {
+ autowiredSb.append("/*\n");
+ commentLines.forEach(c -> autowiredSb.append(" * ").append(c).append("\n"));
+ autowiredSb.append(" */\n");
+ return Optional.of(autowiredSb.toString());
+ }
+
+ return Optional.empty();
+ }
+
+}
diff --git a/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/ejb/actions/MigrateEjbDeploymentDescriptor.java b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/ejb/actions/MigrateEjbDeploymentDescriptor.java
new file mode 100644
index 000000000..9559483db
--- /dev/null
+++ b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/ejb/actions/MigrateEjbDeploymentDescriptor.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2021 - 2023 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.springframework.sbm.jee.ejb.actions;
+
+import org.springframework.sbm.engine.recipe.AbstractAction;
+import org.springframework.sbm.java.api.JavaSource;
+import org.springframework.sbm.java.api.Type;
+import org.springframework.sbm.engine.context.ProjectContext;
+import org.springframework.sbm.jee.ejb.api.DescriptionType;
+import org.springframework.sbm.jee.ejb.api.SessionBeanType;
+import org.springframework.sbm.jee.ejb.api.EjbJarXml;
+import org.springframework.sbm.jee.ejb.filter.EjbJarXmlResourceFinder;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+@NoArgsConstructor
+@SuperBuilder
+public class MigrateEjbDeploymentDescriptor extends AbstractAction {
+ @Override
+ public void apply(ProjectContext context) {
+ Optional ejbJarXml = context.search(new EjbJarXmlResourceFinder());
+ if (ejbJarXml.isPresent()) {
+ migrateEjbDeploymentDescriptor(context, ejbJarXml.get());
+ }
+ }
+
+ private void migrateEjbDeploymentDescriptor(ProjectContext context, EjbJarXml ejbJarXml) {
+
+ List sessionBeansToRemove = new ArrayList<>();
+
+ if (ejbJarXml.getEjbJarXml().getEnterpriseBeans() != null) {
+ ejbJarXml.getEjbJarXml().getEnterpriseBeans().getSessionOrEntityOrMessageDriven()
+ .forEach(bean -> {
+ if (bean.getClass().isAssignableFrom(SessionBeanType.class)) {
+ SessionBeanType sbt = (SessionBeanType) bean;
+ String sessionBeanType = sbt.getSessionType().getValue();
+ if ("Stateless".equals(sessionBeanType)) {
+ handleStatelessSessionBean(context, sbt);
+ // FIXME: #469
+ sessionBeansToRemove.add(sbt);
+ }
+ }
+ });
+ }
+ ejbJarXml.removeSessionBeans(sessionBeansToRemove);
+
+ ejbJarXml.getEjbJarXml().getEnterpriseBeans().getSessionOrEntityOrMessageDriven()
+ .forEach(bean -> {
+ if (bean.getClass().isAssignableFrom(SessionBeanType.class)) {
+ SessionBeanType sbt = (SessionBeanType) bean;
+ ejbJarXml.removeSessionBean(sbt);
+ }
+ });
+
+
+ // delete deployment descriptor when empty
+ if (ejbJarXml.isEmpty()) {
+ ejbJarXml.delete();
+ }
+ }
+
+ private void handleStatelessSessionBean(ProjectContext context, SessionBeanType sbt) {
+ String fqName = sbt.getEjbClass().getValue();
+ Optional extends JavaSource> javaSourceDeclaringType = context.getProjectJavaSources().findJavaSourceDeclaringType(fqName);
+ if (javaSourceDeclaringType.isPresent()) {
+ JavaSource js = javaSourceDeclaringType.get();
+ Type type = js.getType(fqName);
+
+ // remove existing @Stateless annotation if exists
+ String statelessAnnotationFqName = "javax.ejb.Stateless";
+ if (type.hasAnnotation(statelessAnnotationFqName)) {
+ type.removeAnnotation(statelessAnnotationFqName);
+ }
+
+ // add @Stateless annotation as defined in deployment descriptor
+ final StringBuilder statelessSnippet =
+ new StringBuilder("@Stateless(")
+ .append(getEjbName(sbt)
+ .map(n -> "name = \"" + n + "\"")
+ .orElse("")
+ ).append(getMappedName(sbt)
+ .map(m -> ", mappedName = \"" + m + "\"")
+ .orElse("")
+ ).append(getDescription(sbt)
+ .map(d -> ", description = \"" + d + "\"")
+ .orElse("")
+ ).append(")");
+
+ if(statelessSnippet.length() == "@Statlesss()".length())
+ statelessSnippet.deleteCharAt("@Statlesss".length() - 1);
+
+ type.addAnnotation(statelessSnippet.toString(), "javax.ejb.Stateless");
+
+ getRemote(sbt)
+ .map( r -> "@Remote(" + r + ".class)")
+ .ifPresent( a -> type.addAnnotation(a,"javax.ejb.Remote"));
+
+ getRemoteHome(sbt)
+ .map( r -> "@RemoteHome(" + r + ".class)")
+ .ifPresent( a -> type.addAnnotation(a,"javax.ejb.RemoteHome"));
+
+ getLocal(sbt)
+ .map( r -> "@Local(" + r + ".class)")
+ .ifPresent( a -> type.addAnnotation(a,"javax.ejb.Local"));
+
+ getLocalHome(sbt)
+ .map( r -> "@LocalHome(" + r + ".class)")
+ .ifPresent( a -> type.addAnnotation(a,"javax.ejb.LocalHome"));
+
+ getTransactionType(sbt)
+ .map( r -> "@TransactionManagement(javax.ejb.TransactionManagementType." + r.toUpperCase() + ")")
+ .ifPresent( a -> type.addAnnotation(a,"javax.ejb.TransactionManagement"));
+
+ } else {
+ throw new RuntimeException("Could not find any Java file declaring type '" + fqName + "'");
+ }
+ }
+
+ private Optional getEjbName(SessionBeanType sbt) {
+ return sbt.getEjbName() != null && !sbt.getEjbName().getValue().isEmpty()
+ ? Optional.ofNullable(sbt.getEjbName().getValue())
+ : Optional.empty();
+ }
+ private Optional getMappedName(SessionBeanType sbt){
+ return sbt.getMappedName() != null && !sbt.getMappedName().getValue().isEmpty()
+ ? Optional.ofNullable(sbt.getMappedName().getValue())
+ : Optional.empty();
+
+ }
+
+ //FIXME Add support for other languages in description. Currently description attribute of
+ // @Stateless annotation is not a list
+ private Optional getDescription(SessionBeanType sbt) {
+ return sbt.getDescription() != null ? sbt.getDescription()
+ .stream()
+ .filter( d ->"en".equalsIgnoreCase(d.getLang()))
+ .map(DescriptionType::getValue)
+ .findFirst()
+ : Optional.empty();
+ }
+
+ private Optional getRemote(SessionBeanType sbt){
+ return sbt.getRemote() != null && !sbt.getRemote().getValue().isEmpty()
+ ? Optional.ofNullable(sbt.getRemote().getValue())
+ : Optional.empty();
+ }
+
+ private Optional getRemoteHome(SessionBeanType sbt){
+ return sbt.getHome() != null && !sbt.getHome().getValue().isEmpty()
+ ? Optional.ofNullable(sbt.getHome().getValue())
+ : Optional.empty();
+ }
+
+ private Optional getLocal(SessionBeanType sbt){
+ return sbt.getLocal() != null && !sbt.getLocal().getValue().isEmpty()
+ ? Optional.ofNullable(sbt.getLocal().getValue())
+ : Optional.empty();
+ }
+
+ private Optional getLocalHome(SessionBeanType sbt){
+ return sbt.getLocalHome() != null && !sbt.getLocalHome().getValue().isEmpty()
+ ? Optional.ofNullable(sbt.getLocalHome().getValue())
+ : Optional.empty();
+ }
+
+ private Optional getTransactionType(SessionBeanType sbt){
+ return sbt.getTransactionType() != null && !sbt.getTransactionType().getValue().isEmpty()
+ ? Optional.ofNullable(sbt.getTransactionType().getValue())
+ : Optional.empty();
+ }
+}
diff --git a/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/ejb/actions/MigrateJndiLookup.java b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/ejb/actions/MigrateJndiLookup.java
new file mode 100644
index 000000000..ee4a1dc87
--- /dev/null
+++ b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/ejb/actions/MigrateJndiLookup.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2021 - 2023 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.springframework.sbm.jee.ejb.actions;
+
+import org.openrewrite.staticanalysis.RemoveUnusedLocalVariables;
+import org.springframework.rewrite.recipes.GenericOpenRewriteRecipe;
+import org.springframework.sbm.engine.recipe.AbstractAction;
+import org.springframework.sbm.java.api.JavaSource;
+import org.springframework.sbm.engine.context.ProjectContext;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.openrewrite.ExecutionContext;
+import org.openrewrite.Recipe;
+import org.openrewrite.java.AddImport;
+import org.openrewrite.java.JavaIsoVisitor;
+import org.openrewrite.java.JavaTemplate;
+import org.openrewrite.java.RemoveUnusedImports;
+import org.openrewrite.java.format.AutoFormat;
+import org.openrewrite.java.tree.*;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+public class MigrateJndiLookup extends AbstractAction {
+
+ @Override
+ public void apply(ProjectContext context) {
+ context.getProjectJavaSources().list().stream()
+ .filter(js -> js.hasImportStartingWith("javax.naming.InitialContext"))
+ .forEach(sourceWithLookup -> {
+ migrateJndiLookup(sourceWithLookup);
+ });
+ }
+
+ private void migrateJndiLookup(JavaSource sourceWithLookup) {
+ List recipeList = List.of(
+ new GenericOpenRewriteRecipe<>(() -> new MigrateJndiLookupVisitor()),
+ new RemoveUnusedLocalVariables(null),
+ new RemoveUnusedImports(),
+ new GenericOpenRewriteRecipe<>(() -> new AddImport<>("org.springframework.beans.factory.annotation.Autowired", null, false)),
+ new AutoFormat()
+ );
+ sourceWithLookup.apply(recipeList.toArray(new Recipe[]{}));
+ }
+
+ class MigrateJndiLookupVisitor extends JavaIsoVisitor {
+
+ public static final String MATCHES_KEY = "matchingVariables";
+
+ @Override
+ public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration cd, ExecutionContext executionContext) {
+
+ J.ClassDeclaration classDeclaration = super.visitClassDeclaration(cd, executionContext);
+
+ Object matchingVariable = executionContext.getMessage(MATCHES_KEY);
+ if (matchingVariable != null) {
+ List matches = (List) matchingVariable;
+ Iterator iterator = matches.iterator();
+ while (iterator.hasNext()) {
+ MatchFound nextMatch = iterator.next();
+ classDeclaration = addInstanceAsAutowiredMember(classDeclaration, nextMatch);
+ }
+ }
+
+ return classDeclaration;
+ }
+
+ @Override
+ public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations multiVariable, ExecutionContext executionContext) {
+ if (isAssignedThroughStaticInitialContextLookup(multiVariable)) {
+ J.Block containingBlock = findContainingBlock();
+ addToExecutionContext(containingBlock, multiVariable, executionContext);
+ return super.visitVariableDeclarations(multiVariable, executionContext);
+ }
+ return multiVariable;
+ }
+
+ @Override
+ public J.Block visitBlock(J.Block b, ExecutionContext executionContext) {
+ J.Block block = super.visitBlock(b, executionContext);
+
+ if (executionContext.getMessage(MATCHES_KEY) != null) {
+ List matches = executionContext.getMessage(MATCHES_KEY);
+ block = removeFromMethodBlock(matches, block);
+ }
+
+ return block;
+ }
+
+ private J.Block findContainingBlock() {
+ return getCursor().firstEnclosing(J.Block.class);
+ }
+
+ private void addToExecutionContext(J.Block containingBlock, J.VariableDeclarations multiVariable, ExecutionContext executionContext) {
+ MatchFound matchFound = new MatchFound(containingBlock, multiVariable);
+ if (executionContext.getMessage(MATCHES_KEY) != null) {
+ ((List) executionContext.getMessage(MATCHES_KEY)).add(matchFound);
+ } else {
+ List matches = new ArrayList<>();
+ matches.add(matchFound);
+ executionContext.putMessage(MATCHES_KEY, matches);
+ }
+ }
+
+
+ private J.Block removeFromMethodBlock(List matches, J.Block block) {
+ for (MatchFound match : matches) {
+ if (match.getContainingBlock() != null && block.getId().equals(match.getContainingBlock().getId())) {
+ List statements = block.getStatements();
+ Iterator iterator = statements.iterator();
+ while (iterator.hasNext()) {
+ Statement statement = iterator.next();
+ if (statement == match.getMultiVariable()) {
+ iterator.remove();
+ block = block.withStatements(statements);
+ }
+ }
+ } else if (match.getContainingBlock() == null) {
+
+ }
+ }
+ return block;
+ }
+
+ private J.ClassDeclaration addInstanceAsAutowiredMember(J.ClassDeclaration classDecl, MatchFound matchFound) {
+ J.Block body = classDecl.getBody();
+ J.VariableDeclarations variable = matchFound.getMultiVariable();
+ JavaType.Class type = (JavaType.Class) variable.getTypeExpression().getType();
+ String variableName = variable.getVariables().get(0).getSimpleName();
+ JavaTemplate javaTemplate = JavaTemplate.builder("@Autowired\nprivate " + type.getClassName() + " " + variableName).build();
+ J.Block result = javaTemplate.apply(getCursor(), body.getCoordinates().lastStatement());
+ List statements1 = result.getStatements();
+ Statement statement = statements1.get(statements1.size() - 1);
+ statements1.remove(statement);
+ statements1.add(0, statement);
+ result = body.withStatements(statements1);
+ J.ClassDeclaration classDeclaration = classDecl.withBody(result);
+ return classDeclaration;
+ }
+
+ private boolean isAssignedThroughStaticInitialContextLookup(J.VariableDeclarations multiVariable) {
+ if (multiVariable.getVariables().size() != 1) {
+ // TODO: handle multi variable -> maybe refactor multivariable to single variable decl first.
+ } else {
+ JRightPadded namedVariable = multiVariable.getPadding().getVariables().get(0);
+ JLeftPadded variableInitializer = namedVariable.getElement().getPadding().getInitializer();
+ if (variableInitializer.getClass().isAssignableFrom(JLeftPadded.class)) {
+ JLeftPadded jLeftPadded = variableInitializer;
+ Expression element = variableInitializer.getElement();
+ if (element.getClass().isAssignableFrom(J.TypeCast.class)) {
+ J.MethodInvocation methodInvocation = (J.MethodInvocation) ((J.TypeCast) element).getExpression();
+ if (methodInvocation.getSelect().getType().getClass().isAssignableFrom(JavaType.Class.class)) {
+ JavaType.Class type = (JavaType.Class) methodInvocation.getSelect().getType();
+ return type.getFullyQualifiedName().equals("javax.naming.InitialContext");
+ }
+ } else if (element.getClass().isAssignableFrom(J.MethodInvocation.class)) {
+ J.MethodInvocation methodInvocation = (J.MethodInvocation) element;
+ if (methodInvocation.getSelect().getType().getClass().isAssignableFrom(JavaType.Class.class)) {
+ JavaType.Class type = (JavaType.Class) methodInvocation.getSelect().getType();
+ return type.getFullyQualifiedName().equals("javax.naming.InitialContext");
+ }
+ }
+ }
+
+ }
+ return false;
+ }
+
+ @RequiredArgsConstructor
+ @Getter
+ private class MatchFound {
+ private final J.Block containingBlock;
+ private final J.VariableDeclarations multiVariable;
+ }
+ }
+
+
+}
diff --git a/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/ejb/actions/MigrateLocalStatelessSessionBeans.java b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/ejb/actions/MigrateLocalStatelessSessionBeans.java
new file mode 100644
index 000000000..e97475991
--- /dev/null
+++ b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/ejb/actions/MigrateLocalStatelessSessionBeans.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2021 - 2023 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.springframework.sbm.jee.ejb.actions;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.sbm.engine.context.ProjectContext;
+import org.springframework.sbm.engine.recipe.AbstractAction;
+import org.springframework.sbm.java.api.JavaSource;
+import org.springframework.sbm.java.api.SuperTypeHierarchy;
+import org.springframework.sbm.java.api.Type;
+
+import java.util.List;
+
+/**
+ * Migrate local stateless Session Beans to Spring beans annotated with {@code @Service}.
+ *
+ *
+ *
+ * - For NoInterfaceView Session Beans the {@code @Stateless} annotation is replaced with {@code @Service}.
+ * - For all other Session Beans not implementing a {@code @Remote} business interface
+ *
+ * - the {@code @Local} interface information is removed.
+ * - the {@code @Stateless} annotation is replaced with {@code @Service}.
+ *
+ * - Session Beans implementing a {@code @Remote} interface are ignored.
+ *
+ *
+ */
+@RequiredArgsConstructor
+public class MigrateLocalStatelessSessionBeans extends AbstractAction {
+
+ private final MigrateLocalStatelessSessionBeansHelper helper;
+
+ public MigrateLocalStatelessSessionBeans() {
+ helper = new MigrateLocalStatelessSessionBeansHelper();
+ }
+
+ @Override
+ public void apply(ProjectContext context) {
+ migrateLocalStatelessSessionBeans(context);
+ migrateSingletonSessionBeans(context);
+ removeLocalBeanAnnotations(context);
+ }
+
+ private void removeLocalBeanAnnotations(ProjectContext context) {
+ helper.removeLocalBeanAnnotations(context.getProjectJavaSources());
+ }
+
+ private void migrateSingletonSessionBeans(ProjectContext context) {
+ List singletonBeans = helper.findTypesAnnotatedWithSingleton(context.getProjectJavaSources());
+ singletonBeans.forEach(ssb -> {
+ Type type = ssb.getType();
+ helper.migrateSingletonAnnotation(type);
+ });
+ }
+
+ private void migrateLocalStatelessSessionBeans(ProjectContext context) {
+ List statelessSessionBeans = helper.findTypesAnnotatedWithStateless(context.getProjectJavaSources());
+
+ statelessSessionBeans.forEach(ssb -> {
+ Type type = ssb.getType();
+ JavaSource javaSource = ssb.getJavaSource();
+ SuperTypeHierarchy hierarchy = new SuperTypeHierarchy(type);
+ if (helper.implementsNoRemoteInterface(hierarchy)) {
+ helper.removeLocalAnnotations(hierarchy);
+ helper.migrateStatelessAnnotation(type);
+ }
+ });
+ }
+
+}
diff --git a/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/ejb/actions/MigrateLocalStatelessSessionBeansHelper.java b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/ejb/actions/MigrateLocalStatelessSessionBeansHelper.java
new file mode 100644
index 000000000..529854cab
--- /dev/null
+++ b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/ejb/actions/MigrateLocalStatelessSessionBeansHelper.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2021 - 2023 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.springframework.sbm.jee.ejb.actions;
+
+import org.springframework.sbm.java.api.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MigrateLocalStatelessSessionBeansHelper {
+
+ public static final String EJB_STATELESS_ANNOTATION = "javax.ejb.Stateless";
+ public static final String EJB_LOCAL_ANNOTATION = "javax.ejb.Local";
+ private static final String SPRING_SERVICE_ANNOTATION = "org.springframework.stereotype.Service";
+ private static final String EJB_REMOTE_ANNOTATION = "javax.ejb.Remote";
+ private static final String EJB_SINGLETON_ANNOTATION = "javax.ejb.Singleton";
+ private static final String EJB_LOCALBEAN_ANNOTATION = "javax.ejb.LocalBean";
+
+ public boolean implementsNoRemoteInterface(SuperTypeHierarchy hierarchy) {
+ boolean noRemoteInterface = new AnnotatedTypeFinder(hierarchy, EJB_REMOTE_ANNOTATION).findAnnotatedTypes().isEmpty();
+ return noRemoteInterface || hierarchy.getRoot().getSuperTypes().isEmpty();
+ }
+
+ public void removeLocalAnnotations(SuperTypeHierarchy hierarchy) {
+ List annotatedTypes = new AnnotatedTypeFinder(hierarchy, EJB_LOCAL_ANNOTATION).findAnnotatedTypes();
+ annotatedTypes.forEach(type -> type.removeAnnotation(EJB_LOCAL_ANNOTATION));
+ }
+
+ public List findTypesAnnotatedWithStateless(ProjectJavaSources javaSourceSet) {
+ return getTypeAndSourceFiles(javaSourceSet, EJB_STATELESS_ANNOTATION);
+ }
+
+ private List getTypeAndSourceFiles(ProjectJavaSources javaSourceSet, String annotation) {
+ List typeAndSourceFiles = new ArrayList<>();
+ javaSourceSet.list().stream()
+ .forEach(js -> {
+ js.getTypes().stream()
+ .filter(t -> {
+ return t.hasAnnotation(annotation);
+ })
+ .forEach(t -> typeAndSourceFiles.add(new TypeAndSourceFile(js, t)));
+ });
+ return typeAndSourceFiles;
+ }
+
+ public void migrateStatelessAnnotation(Type ssb) {
+ StatelessAnnotationTemplateMapper statelessAnnotationTemplateMapper = new StatelessAnnotationTemplateMapper();
+ Annotation annotation = ssb.getAnnotations().stream().filter(a -> EJB_STATELESS_ANNOTATION.equals(a.getFullyQualifiedName())).findFirst().get();
+ String annotationSnippet = statelessAnnotationTemplateMapper.mapToServiceAnnotation(annotation);
+ ssb.removeAnnotation(EJB_STATELESS_ANNOTATION);
+ ssb.addAnnotation(annotationSnippet, SPRING_SERVICE_ANNOTATION);
+ }
+
+ public List findTypesAnnotatedWithSingleton(ProjectJavaSources javaSourceSet) {
+ return getTypeAndSourceFiles(javaSourceSet, EJB_SINGLETON_ANNOTATION);
+ }
+
+ public void migrateSingletonAnnotation(Type ssb) {
+ SingletonAnnotationTemplateMapper statelessAnnotationTemplateMapper = new SingletonAnnotationTemplateMapper();
+ Annotation annotation = ssb.getAnnotations().stream().filter(a -> EJB_SINGLETON_ANNOTATION.equals(a.getFullyQualifiedName())).findFirst().get();
+ String annotationSnippet = statelessAnnotationTemplateMapper.mapToServiceAnnotation(annotation);
+ ssb.removeAnnotation(EJB_SINGLETON_ANNOTATION);
+ ssb.addAnnotation(annotationSnippet, SPRING_SERVICE_ANNOTATION);
+ }
+
+ public List findTypesAnnotatedWithLocalBean(ProjectJavaSources javaSourceSet) {
+ return getTypeAndSourceFiles(javaSourceSet, EJB_LOCALBEAN_ANNOTATION);
+ }
+
+ public void removeLocalBeanAnnotations(ProjectJavaSources javaSourceSet) {
+ findTypesAnnotatedWithLocalBean(javaSourceSet)
+ .forEach(ts -> ts.getType().removeAnnotation(EJB_LOCALBEAN_ANNOTATION));
+ }
+
+ private class AnnotatedTypeFinder {
+
+ private final SuperTypeHierarchy superTypeHierarchy;
+ private final String annotationToFind;
+ private final List annotatedTypes = new ArrayList<>();
+
+ public AnnotatedTypeFinder(SuperTypeHierarchy superTypeHierarchy, String annotationToFind) {
+ this.superTypeHierarchy = superTypeHierarchy;
+ this.annotationToFind = annotationToFind;
+ }
+
+ public List findAnnotatedTypes() {
+ traverseHierarchyUpAndSearchForAnnotation(superTypeHierarchy.getRoot());
+ return annotatedTypes;
+ }
+
+ private void traverseHierarchyUpAndSearchForAnnotation(SuperTypeHierarchyNode currentNode) {
+ if (currentNode.getNode().hasAnnotation(annotationToFind)) {
+ this.annotatedTypes.add(currentNode.getNode());
+ }
+ if (!currentNode.getSuperTypes().isEmpty()) {
+ currentNode.getSuperTypes().forEach(sthn -> traverseHierarchyUpAndSearchForAnnotation(sthn));
+ }
+ }
+
+ }
+}
diff --git a/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/ejb/actions/SingletonAnnotationTemplateMapper.java b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/ejb/actions/SingletonAnnotationTemplateMapper.java
new file mode 100644
index 000000000..5b98f453a
--- /dev/null
+++ b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/ejb/actions/SingletonAnnotationTemplateMapper.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2021 - 2023 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.springframework.sbm.jee.ejb.actions;
+
+import org.springframework.sbm.java.api.Annotation;
+import org.springframework.sbm.java.api.Expression;
+
+import java.util.Map;
+
+public class SingletonAnnotationTemplateMapper {
+ private static final String DESCRIPTION_ATTRIBUTE = "description";
+ private static final String NAME_ATTRIBUTE = "name";
+ private static final String EJB_SINGLETON_ANNOTATAION_FQN = "javax.ejb.Singleton";
+
+ public String mapToServiceAnnotation(Annotation annotation) {
+
+ if (!EJB_SINGLETON_ANNOTATAION_FQN.equals(annotation.getFullyQualifiedName())) {
+ throw new IllegalArgumentException("Passed invalid annotation '" + annotation.getFullyQualifiedName() + "'");
+ }
+
+ StringBuilder serviceAnnotationBuilder = new StringBuilder();
+
+ Map attributes = annotation.getAttributes();
+ if (attributes.containsKey(DESCRIPTION_ATTRIBUTE)) {
+ serviceAnnotationBuilder.append("/**").append("\n");
+ String descriptionValue = attributes.get(DESCRIPTION_ATTRIBUTE).getAssignmentRightSide().printVariable();
+ serviceAnnotationBuilder.append("* ").append(descriptionValue).append("\n");
+ serviceAnnotationBuilder.append("*/").append("\n");
+ }
+
+ serviceAnnotationBuilder.append("@Service");
+
+ if (attributes.containsKey(NAME_ATTRIBUTE)) {
+ Expression expression = attributes.get(NAME_ATTRIBUTE);
+ String nameAttributeValue = expression.getAssignmentRightSide().printVariable();
+ serviceAnnotationBuilder.append("(\"").append(nameAttributeValue).append("\")");
+ }
+
+ return serviceAnnotationBuilder.toString();
+ }
+}
diff --git a/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/ejb/actions/StatelessAnnotationTemplateMapper.java b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/ejb/actions/StatelessAnnotationTemplateMapper.java
new file mode 100644
index 000000000..a05698454
--- /dev/null
+++ b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/ejb/actions/StatelessAnnotationTemplateMapper.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2021 - 2023 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.springframework.sbm.jee.ejb.actions;
+
+import org.springframework.sbm.java.api.Annotation;
+import org.springframework.sbm.java.api.Expression;
+
+import java.util.Map;
+
+/**
+ * Helps with the migration of {@code @Stateless} annotation to {@code @Service} annotation.
+ */
+public class StatelessAnnotationTemplateMapper {
+
+ private static final String DESCRIPTION_ATTRIBUTE = "description";
+ private static final String NAME_ATTRIBUTE = "name";
+ private static final String EJB_STATELESS_ANNOTATAION_FQN = "javax.ejb.Stateless";
+
+ public String mapToServiceAnnotation(Annotation annotation) {
+
+ if (!EJB_STATELESS_ANNOTATAION_FQN.equals(annotation.getFullyQualifiedName())) {
+ throw new IllegalArgumentException("Passed invalid annotation '" + annotation.getFullyQualifiedName() + "'");
+ }
+
+ StringBuilder serviceAnnotationBuilder = new StringBuilder();
+
+ Map attributes = annotation.getAttributes();
+ if (attributes.containsKey(DESCRIPTION_ATTRIBUTE)) {
+ serviceAnnotationBuilder.append("/**").append("\n");
+ String descriptionValue = attributes.get(DESCRIPTION_ATTRIBUTE).getAssignmentRightSide().printVariable();
+ serviceAnnotationBuilder.append("* ").append(descriptionValue).append("\n");
+ serviceAnnotationBuilder.append("*/").append("\n");
+ }
+
+ serviceAnnotationBuilder.append("@Service");
+
+ if (attributes.containsKey(NAME_ATTRIBUTE)) {
+ Expression expression = attributes.get(NAME_ATTRIBUTE);
+ String nameAttributeValue = expression.getAssignmentRightSide().printVariable();
+ serviceAnnotationBuilder.append("(\"").append(nameAttributeValue).append("\")");
+ }
+
+ return serviceAnnotationBuilder.toString();
+ }
+
+}
diff --git a/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/ejb/actions/TypeAndSourceFile.java b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/ejb/actions/TypeAndSourceFile.java
new file mode 100644
index 000000000..48b81851c
--- /dev/null
+++ b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/ejb/actions/TypeAndSourceFile.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2021 - 2023 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.springframework.sbm.jee.ejb.actions;
+
+import org.springframework.sbm.java.api.JavaSource;
+import org.springframework.sbm.java.api.Type;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@Getter
+@RequiredArgsConstructor
+public class TypeAndSourceFile {
+
+ private final JavaSource javaSource;
+ private final Type type;
+
+}
diff --git a/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/ejb/conditions/NoTransactionalAnnotationPresentOnTypeAnnotatedWith.java b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/ejb/conditions/NoTransactionalAnnotationPresentOnTypeAnnotatedWith.java
new file mode 100644
index 000000000..f1636b5ff
--- /dev/null
+++ b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/ejb/conditions/NoTransactionalAnnotationPresentOnTypeAnnotatedWith.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2021 - 2023 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.springframework.sbm.jee.ejb.conditions;
+
+import org.springframework.sbm.engine.recipe.Condition;
+import org.springframework.sbm.engine.context.ProjectContext;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Condition is true if any type found annotated with {@code typeAnnotatedWith} but no {@code org.springframework.transaction.annotation.Transactional} annotation is present.
+ */
+@Getter
+@Setter
+public class NoTransactionalAnnotationPresentOnTypeAnnotatedWith implements Condition {
+
+ private String typeAnnotatedWith;
+ private static final String TRANSACTION_ANNOTATION = "org.springframework.transaction.annotation.Transactional";
+
+ @Override
+ public String getDescription() {
+ return "Add @Transactional annotation to types annotated with " + typeAnnotatedWith + ".";
+ }
+
+ @Override
+ public boolean evaluate(ProjectContext context) {
+ return context.getProjectJavaSources().asStream()
+ .flatMap(js -> js.getTypes().stream())
+ .anyMatch(t -> t.hasAnnotation(typeAnnotatedWith) && !t.hasAnnotation(TRANSACTION_ANNOTATION));
+ }
+}
diff --git a/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/ejb/recipes/MigrateEjbJarDeploymentDescriptorRecipe.java b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/ejb/recipes/MigrateEjbJarDeploymentDescriptorRecipe.java
new file mode 100644
index 000000000..cb48c4ff8
--- /dev/null
+++ b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/ejb/recipes/MigrateEjbJarDeploymentDescriptorRecipe.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2021 - 2023 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.springframework.sbm.jee.ejb.recipes;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.sbm.common.migration.conditions.FileMatchingPatternExist;
+import org.springframework.sbm.engine.recipe.Recipe;
+import org.springframework.sbm.jee.ejb.actions.MigrateEjbDeploymentDescriptor;
+
+@Configuration
+public class MigrateEjbJarDeploymentDescriptorRecipe {
+ @Bean
+ public Recipe ejbJarDeploymentDescriptor() {
+ return Recipe.builder()
+ .name("migrate-ejb-jar-deployment-descriptor")
+ .order(90)
+ .description("Add or overrides @Stateless annotation as defined in ejb deployment descriptor")
+ .condition(new FileMatchingPatternExist("/**/ejb-jar.xml")) // Recipe condition is True thus it'll be applicable based on applicability of actions
+ .action(
+ MigrateEjbDeploymentDescriptor.builder()
+ .condition(
+ FileMatchingPatternExist.builder()
+ .pattern("/**/ejb-jar.xml")
+ .build()
+ )
+ .build()
+ )
+ .build();
+ }
+}
diff --git a/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/jaxrs/MigrateJaxRsRecipe.java b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/jaxrs/MigrateJaxRsRecipe.java
new file mode 100644
index 000000000..63be82df8
--- /dev/null
+++ b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/jaxrs/MigrateJaxRsRecipe.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2021 - 2023 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.springframework.sbm.jee.jaxrs;
+
+import org.openrewrite.java.ChangeType;
+import org.openrewrite.java.JavaParser;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.sbm.build.api.Dependency;
+import org.springframework.sbm.build.migration.actions.AddDependencies;
+import org.springframework.sbm.build.migration.conditions.NoExactDependencyExist;
+import org.springframework.sbm.engine.recipe.OpenRewriteDeclarativeRecipeAdapter;
+import org.springframework.sbm.engine.recipe.Recipe;
+import org.springframework.sbm.engine.recipe.RewriteRecipeLoader;
+import org.springframework.sbm.java.JavaRecipeAction;
+import org.springframework.sbm.java.impl.ClasspathRegistry;
+import org.springframework.sbm.java.migration.actions.ReplaceTypeAction;
+import org.springframework.sbm.java.migration.conditions.HasAnnotation;
+import org.springframework.sbm.java.migration.conditions.HasImportStartingWith;
+import org.springframework.sbm.java.migration.conditions.HasTypeAnnotation;
+import org.springframework.sbm.jee.jaxrs.actions.ConvertJaxRsAnnotations;
+import org.springframework.sbm.jee.jaxrs.recipes.ReplaceMediaType;
+import org.springframework.sbm.jee.jaxrs.recipes.ReplaceRequestParameterProperties;
+import org.springframework.sbm.jee.jaxrs.recipes.SwapCacheControl;
+import org.springframework.sbm.jee.jaxrs.recipes.SwapHttHeaders;
+import org.springframework.sbm.jee.jaxrs.recipes.SwapResponseWithResponseEntity;
+
+import java.util.List;
+import java.util.function.Supplier;
+
+@Configuration
+//@RequiredArgsConstructor
+public class MigrateJaxRsRecipe {
+
+
+ @Bean
+ public Recipe jaxRs(RewriteRecipeLoader rewriteRecipeLoader) {
+ return Recipe.builder()
+ .name("migrate-jax-rs")
+ .order(60)
+ .description("Any class has import starting with javax.ws.rs")
+ .condition(HasImportStartingWith.builder().value("javax.ws.rs").description("Any class has import starting with javax.ws.rs").build())
+ .actions(List.of(
+
+// AddDependencies.builder()
+// .dependencies(
+// List.of(
+// Dependency.builder().groupId("org.springframework.boot").artifactId("spring-boot-starter-web").version("2.3.4.RELEASE").build()
+// )
+// )
+// .description("Add spring-boot-starter-web dependency to build file.")
+// .condition(NoExactDependencyExist.builder().dependency(Dependency.builder().groupId("org.springframework.boot").artifactId("spring-boot-starter-web").build()).build())
+// .build(),
+
+ ConvertJaxRsAnnotations.builder()
+ .condition(HasTypeAnnotation.builder().annotation("javax.ws.rs.Path").build())
+ .description("Convert JAX-RS annotations into Spring Boot annotations.")
+ .build(),
+
+ // Important! Replace method parameter annotations after ConvertJaxRsAnnotations action
+ // ConvertJaxRsAnnotations examines Jax-Rs annotations on the parameters to determine the request body param
+
+ ReplaceTypeAction.builder()
+ .condition(HasAnnotation.builder().annotation("javax.ws.rs.PathParam").build())
+ .description("Replace JAX-RS @PathParam with Spring Boot @PathVariable annotation.")
+ .existingType("javax.ws.rs.PathParam")
+ .withType("org.springframework.web.bind.annotation.PathVariable")
+ .build(),
+
+ ReplaceTypeAction.builder()
+ .condition(HasAnnotation.builder().annotation("javax.ws.rs.QueryParam").build())
+ .description("Replace JAX-RS @QueryParam with Spring Boot @RequestParam annotation.")
+ .existingType("javax.ws.rs.QueryParam")
+ .withType("org.springframework.web.bind.annotation.RequestParam")
+ .build(),
+
+ ReplaceTypeAction.builder()
+ .condition(HasAnnotation.builder().annotation("javax.ws.rs.FormParam").build())
+ .description("Replace JAX-RS @FormParam with Spring Boot @RequestParam annotation.")
+ .existingType("javax.ws.rs.QueryParam")
+ .withType("org.springframework.web.bind.annotation.RequestParam")
+ .build(),
+
+ JavaRecipeAction.builder()
+ .condition(HasImportStartingWith.builder().value("javax.ws.rs.core.MediaType").build())
+ .description("Replace JaxRs MediaType with it's Spring equivalent.")
+ .recipe(new ReplaceMediaType())
+ .build(),
+
+ JavaRecipeAction.builder()
+ .condition(HasImportStartingWith.builder().value("javax.ws.rs.core.HttpHeaders").build())
+ .description("Replace JaxRs HttpHeaders with it's Spring equivalent.")
+ .recipe(new SwapHttHeaders())
+ .build(),
+
+ JavaRecipeAction.builder()
+ .condition(HasImportStartingWith.builder().value("javax.ws.rs.core.MultivaluedMap").build())
+ .description("Replace JaxRs MultivaluedMap with it's Spring equivalent.")
+ .recipe(new ChangeType("javax.ws.rs.core.MultivaluedMap", "org.springframework.util.MultiValueMap", false))
+ .build(),
+
+ JavaRecipeAction.builder()
+ .condition(HasImportStartingWith.builder().value("javax.ws.rs.core.CacheControl").build())
+ .description("Replace JaxRs CacheControl with it's Spring equivalent.")
+ .recipe(new SwapCacheControl())
+ .build(),
+
+ JavaRecipeAction.builder()
+ .condition(HasImportStartingWith.builder().value("javax.ws.rs.core.Response").build())
+ .description("Replace JaxRs Response and ResponseBuilder with it's Spring equivalent.")
+ .recipe(new SwapResponseWithResponseEntity())
+ .build(),
+
+ JavaRecipeAction.builder()
+ .condition(HasAnnotation.builder().annotation("org.springframework.web.bind.annotation.RequestParam").build())
+ .description("Replace the JAX-RS properties of a request parameter (like default value) with it's Spring equivalent.")
+ .recipe(new ReplaceRequestParameterProperties())
+ .build(),
+
+ OpenRewriteDeclarativeRecipeAdapter.builder()
+ .condition(HasAnnotation.builder().annotation("org.springframework.web.bind.annotation.RequestParam").build())
+ .description("Adds required=false to all @RequestParam annotations")
+ .rewriteRecipeLoader(rewriteRecipeLoader)
+ .openRewriteRecipe(
+ """
+ type: specs.openrewrite.org/v1beta/recipe
+ name: org.springframework.sbm.jee.MakeRequestParamsOptional
+ displayName: Set required=false for @RequestParam without 'required'
+ description: Set required=false for @RequestParam without 'required'
+ causesAnotherCycle: true
+ recipeList:
+ - org.openrewrite.java.AddOrUpdateAnnotationAttribute:
+ annotationType: "org.springframework.web.bind.annotation.RequestParam"
+ attributeName: "required"
+ attributeValue: "false"
+ addOnly: true
+ """)
+ .build()
+ )
+ )
+ .build();
+ }
+
+}
diff --git a/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/jaxrs/actions/ConvertJaxRsAnnotations.java b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/jaxrs/actions/ConvertJaxRsAnnotations.java
new file mode 100644
index 000000000..652805cc0
--- /dev/null
+++ b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/jaxrs/actions/ConvertJaxRsAnnotations.java
@@ -0,0 +1,325 @@
+/*
+ * Copyright 2021 - 2023 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.springframework.sbm.jee.jaxrs.actions;
+
+import org.springframework.sbm.engine.recipe.AbstractAction;
+import org.springframework.sbm.java.api.*;
+import org.springframework.sbm.engine.context.ProjectContext;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+
+import java.util.*;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+
+@NoArgsConstructor
+@SuperBuilder
+public class ConvertJaxRsAnnotations extends AbstractAction {
+
+ public static final Pattern JAXRS_ANNOTATION_PATTERN = Pattern.compile("javax\\.ws\\.rs\\..*");
+ public static final Pattern SPRING_ANNOTATION_PATTERN = Pattern.compile("org\\.springframework\\.web\\.bind\\..*");
+
+ @Override
+ public void apply(ProjectContext context) {
+ for (JavaSource js : context.getProjectJavaSources().list()) {
+ for (Type t : js.getTypes()) {
+ if (t.hasAnnotation("javax.ws.rs.Path")) {
+ transform(t);
+ }
+ }
+ }
+ }
+
+ private void transform(Type type) {
+ transformTypeAnnotations(type);
+ transformMethodAnnotations(type);
+ }
+
+ private void transformMethodAnnotations(Type type) {
+ type.getMethods().stream()
+ .filter(this::isJaxRsMethod)
+ .forEach(this::convertJaxRsMethodToSpringMvc);
+ }
+
+ void convertJaxRsMethodToSpringMvc(Method method) {
+ Map attrs = new LinkedHashMap<>();
+ Set methods = new LinkedHashSet<>();
+ var annotations = method.getAnnotations();
+
+ // Add @RequestBody over the first non-annotated parameter without other jax-rs annotations
+ method.getParams().stream()
+ .filter(p -> !p.containsAnnotation(JAXRS_ANNOTATION_PATTERN))
+ .filter(p -> !p.containsAnnotation(SPRING_ANNOTATION_PATTERN))
+ .findFirst()
+ .ifPresent(p -> p.addAnnotation("@RequestBody", "org.springframework.web.bind.annotation.RequestBody"));
+
+ for (Annotation a : annotations) {
+ if (a == null) {
+ continue;
+ }
+ String fullyQualifiedName = a.getFullyQualifiedName();
+ if (fullyQualifiedName != null) {
+ switch (fullyQualifiedName) {
+ case "javax.ws.rs.Path":
+ attrs.put("value", a.getAttribute("value"));
+ method.removeAnnotation(a);
+ break;
+ case "javax.ws.rs.Consumes":
+ attrs.put("consumes", a.getAttribute("value"));
+ method.removeAnnotation(a);
+ break;
+ case "javax.ws.rs.Produces":
+ attrs.put("produces", a.getAttribute("value"));
+ method.removeAnnotation(a);
+ break;
+ case "javax.ws.rs.POST":
+ methods.add("POST");
+ method.removeAnnotation(a);
+ break;
+ case "javax.ws.rs.GET":
+ methods.add("GET");
+ method.removeAnnotation(a);
+ break;
+ case "javax.ws.rs.PUT":
+ methods.add("PUT");
+ method.removeAnnotation(a);
+ break;
+ case "javax.ws.rs.DELETE":
+ methods.add("DELETE");
+ method.removeAnnotation(a);
+ break;
+ case "javax.ws.rs.HEAD":
+ methods.add("HEAD");
+ method.removeAnnotation(a);
+ break;
+ case "javax.ws.rs.PATCH":
+ methods.add("PATCH");
+ method.removeAnnotation(a);
+ break;
+ case "javax.ws.rs.TRACE":
+ methods.add("TRACE");
+ method.removeAnnotation(a);
+ break;
+ default:
+ }
+ }
+ }
+
+ if (method.getAnnotations().size() < annotations.size()) {
+ StringBuilder sb = new StringBuilder("@RequestMapping");
+ boolean parametersPresent = !(attrs.isEmpty() && methods.isEmpty());
+ if (parametersPresent) {
+ sb.append("(");
+ }
+
+ sb.append(attrs.entrySet().stream()
+ .map(e -> e.getKey() + " = " + e.getValue().print())
+ .collect(Collectors.joining(", ")));
+
+ if (!methods.isEmpty()) {
+ if(!attrs.entrySet().isEmpty()) {
+ sb.append(", ");
+ }
+ if (methods.size() == 1) {
+ sb.append("method = RequestMethod." + methods.iterator().next());
+ } else {
+ sb.append("method = {");
+ sb.append(methods.stream().map(m -> "RequestMethod." + m).collect(Collectors.joining(", ")));
+ sb.append("}");
+ }
+ }
+ if (parametersPresent) {
+ sb.append(")");
+ }
+
+ Set typeStubs = Set.of(
+ """
+ package org.springframework.web.bind.annotation;
+ import java.lang.annotation.Documented;
+ import java.lang.annotation.ElementType;
+ import java.lang.annotation.Retention;
+ import java.lang.annotation.RetentionPolicy;
+ import java.lang.annotation.Target;
+ import org.springframework.aot.hint.annotation.Reflective;
+ import org.springframework.core.annotation.AliasFor;
+ @Target({ElementType.TYPE, ElementType.METHOD})
+ @Retention(RetentionPolicy.RUNTIME)
+ @Documented
+ @Mapping
+ @Reflective(ControllerMappingReflectiveProcessor.class)
+ public @interface RequestMapping {
+ String name() default "";
+ @AliasFor("path")
+ String[] value() default {};
+ @AliasFor("value")
+ String[] path() default {};
+ RequestMethod[] method() default {};
+ String[] params() default {};
+ String[] headers() default {};
+ String[] consumes() default {};
+ String[] produces() default {};
+
+ }
+ """,
+ """
+ package org.springframework.web.bind.annotation;
+
+ import org.springframework.http.HttpMethod;
+ import org.springframework.lang.Nullable;
+ import org.springframework.util.Assert;
+ public enum RequestMethod {
+
+ GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE;
+
+ @Nullable
+ public static RequestMethod resolve(String method) {
+ Assert.notNull(method, "Method must not be null");
+ return switch (method) {
+ case "GET" -> GET;
+ case "HEAD" -> HEAD;
+ case "POST" -> POST;
+ case "PUT" -> PUT;
+ case "PATCH" -> PATCH;
+ case "DELETE" -> DELETE;
+ case "OPTIONS" -> OPTIONS;
+ case "TRACE" -> TRACE;
+ default -> null;
+ };
+ }
+ @Nullable
+ public static RequestMethod resolve(HttpMethod httpMethod) {
+ Assert.notNull(httpMethod, "HttpMethod must not be null");
+ return resolve(httpMethod.name());
+ }
+ public HttpMethod asHttpMethod() {
+ return switch (this) {
+ case GET -> HttpMethod.GET;
+ case HEAD -> HttpMethod.HEAD;
+ case POST -> HttpMethod.POST;
+ case PUT -> HttpMethod.PUT;
+ case PATCH -> HttpMethod.PATCH;
+ case DELETE -> HttpMethod.DELETE;
+ case OPTIONS -> HttpMethod.OPTIONS;
+ case TRACE -> HttpMethod.TRACE;
+ };
+ }
+ }
+ """
+ );
+
+ method.addAnnotation(sb.toString(), "org.springframework.web.bind.annotation.RequestMapping", typeStubs, "org.springframework.web.bind.annotation.RequestMethod");
+
+ }
+ }
+
+ private boolean isJaxRsMethod(Method method) {
+ return method.containsAnnotation(JAXRS_ANNOTATION_PATTERN);
+ }
+
+ private void transformTypeAnnotations(Type type) {
+ List annotations = type.getAnnotations();
+ Optional found = annotations.stream().filter(a -> "javax.ws.rs.Path".equals(a.getFullyQualifiedName())).findFirst();
+ if (found.isPresent()) {
+ type.removeAnnotation(found.get());
+ String fqName = "org.springframework.web.bind.annotation.RestController";
+ String snippet = "@" + fqName.substring(fqName.lastIndexOf('.') + 1);
+ Set stubs = Set.of("""
+ package org.springframework.web.bind.annotation;
+
+ import java.lang.annotation.Documented;
+ import java.lang.annotation.ElementType;
+ import java.lang.annotation.Retention;
+ import java.lang.annotation.RetentionPolicy;
+ import java.lang.annotation.Target;
+
+ import org.springframework.core.annotation.AliasFor;
+ import org.springframework.stereotype.Controller;
+ @Target(ElementType.TYPE)
+ @Retention(RetentionPolicy.RUNTIME)
+ @Documented
+ @Controller
+ @ResponseBody
+ public @interface RestController {
+ @AliasFor(annotation = Controller.class)
+ String value() default "";
+ }
+ """);
+ type.addAnnotation(snippet, "org.springframework.web.bind.annotation.RestController", stubs);
+ Map attributes = new LinkedHashMap<>(found.get().getAttributes());
+ for (Annotation a : annotations) {
+ if (a == null) {
+ continue;
+ }
+ String fullyQualifiedName = a.getFullyQualifiedName();
+ if (fullyQualifiedName != null) {
+ switch (fullyQualifiedName) {
+ case "javax.ws.rs.Consumes" -> {
+ attributes.put("consumes", a.getAttribute("value"));
+ type.removeAnnotation(a);
+ }
+ case "javax.ws.rs.Produces" -> {
+ attributes.put("produces", a.getAttribute("value"));
+ type.removeAnnotation(a);
+ }
+ default -> {
+ }
+ }
+ }
+ }
+ String rmAttrs = attributes.entrySet().stream().map(e -> e.getKey() + " = " + e.getValue().print()).collect(Collectors.joining(", "));
+ type.addAnnotation("@RequestMapping(" + rmAttrs + ")", "org.springframework.web.bind.annotation.RequestMapping", Set.of("""
+ package org.springframework.web.bind.annotation;
+
+ import java.lang.annotation.Documented;
+ import java.lang.annotation.ElementType;
+ import java.lang.annotation.Retention;
+ import java.lang.annotation.RetentionPolicy;
+ import java.lang.annotation.Target;
+
+ import org.springframework.aot.hint.annotation.Reflective;
+ import org.springframework.core.annotation.AliasFor;
+
+ @Target({ElementType.TYPE, ElementType.METHOD})
+ @Retention(RetentionPolicy.RUNTIME)
+ @Documented
+ @Mapping
+ @Reflective(ControllerMappingReflectiveProcessor.class)
+ public @interface RequestMapping {
+
+ String name() default "";
+
+ @AliasFor("path")
+ String[] value() default {};
+
+ @AliasFor("value")
+ String[] path() default {};
+
+ RequestMethod[] method() default {};
+
+ String[] params() default {};
+
+ String[] headers() default {};
+
+ String[] consumes() default {};
+
+ String[] produces() default {};
+
+ }
+ """));
+ }
+ }
+}
diff --git a/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/jaxrs/recipes/CopyAnnotationAttribute.java b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/jaxrs/recipes/CopyAnnotationAttribute.java
new file mode 100644
index 000000000..c0d640c64
--- /dev/null
+++ b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/jaxrs/recipes/CopyAnnotationAttribute.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2021 - 2023 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.springframework.sbm.jee.jaxrs.recipes;
+
+import lombok.EqualsAndHashCode;
+import lombok.Value;
+import org.jetbrains.annotations.NotNull;
+import org.openrewrite.ExecutionContext;
+import org.openrewrite.Option;
+import org.openrewrite.Recipe;
+import org.openrewrite.TreeVisitor;
+import org.openrewrite.java.JavaIsoVisitor;
+import org.openrewrite.java.search.UsesType;
+import org.springframework.sbm.jee.jaxrs.recipes.visitors.CopyAnnotationAttributeVisitor;
+
+/**
+ * @author Vincent Botteman
+ */
+@Value
+@EqualsAndHashCode(callSuper = true)
+public class CopyAnnotationAttribute extends Recipe {
+ @Option(displayName = "Source Annotation Type",
+ description = "The fully qualified name of the source annotation.",
+ example = "org.junit.Test")
+ String sourceAnnotationType;
+
+ @Option(displayName = "Source Attribute name",
+ description = "The name of the attribute on the source annotation containing the value to copy.",
+ example = "timeout")
+ String sourceAttributeName;
+
+ @Option(displayName = "Target Annotation Type",
+ description = "The fully qualified name of the target annotation.",
+ example = "org.junit.Test")
+ String targetAnnotationType;
+
+ @Option(displayName = "Target Attribute name",
+ description = "The name of the attribute on the target annotation which must be set to the value of the source attribute.",
+ example = "timeout")
+ String targetAttributeName;
+
+ protected TreeVisitor, ExecutionContext> getSingleSourceApplicableTest() {
+ return new UsesType<>(sourceAnnotationType, true);
+ }
+
+ @Override
+ public @NotNull String getDisplayName() {
+ return "Copy an annotation attribute to another annotation";
+ }
+
+ @Override
+ public @NotNull String getDescription() {
+ return "Copy the value of an annotation attribute to another annotation attribute.";
+ }
+
+ @Override
+ @NotNull
+ public JavaIsoVisitor getVisitor() {
+ return new CopyAnnotationAttributeVisitor(
+ sourceAnnotationType,
+ sourceAttributeName,
+ targetAnnotationType,
+ targetAttributeName
+ );
+ }
+}
diff --git a/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/jaxrs/recipes/RemoveAnnotationIfAccompanied.java b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/jaxrs/recipes/RemoveAnnotationIfAccompanied.java
new file mode 100644
index 000000000..9db9f3e5f
--- /dev/null
+++ b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/jaxrs/recipes/RemoveAnnotationIfAccompanied.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2021 - 2023 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.springframework.sbm.jee.jaxrs.recipes;
+
+import lombok.EqualsAndHashCode;
+import lombok.Value;
+import org.jetbrains.annotations.NotNull;
+import org.openrewrite.ExecutionContext;
+import org.openrewrite.Option;
+import org.openrewrite.Recipe;
+import org.openrewrite.TreeVisitor;
+import org.openrewrite.java.search.UsesType;
+import org.springframework.sbm.jee.jaxrs.recipes.visitors.RemoveAnnotationIfAccompaniedVisitor;
+
+/**
+ * @author Vincent Botteman
+ */
+@EqualsAndHashCode(callSuper = true)
+@Value
+public class RemoveAnnotationIfAccompanied extends Recipe {
+ @Option(displayName = "Annotation Type to remove",
+ description = "The fully qualified name of the annotation to remove.",
+ example = "org.junit.Test")
+ String annotationTypeToRemove;
+
+ @Option(displayName = "Annotation Type which must also be present",
+ description = "The fully qualified name of the annotation that must also be present.",
+ example = "org.junit.Test")
+ String additionalAnnotationType;
+
+ @Override
+ public @NotNull String getDisplayName() {
+ return "Remove annotation if accompanied by the other annotation";
+ }
+
+ @Override
+ public @NotNull String getDescription() {
+ return "Remove matching annotation if the other annotation is also present.";
+ }
+
+ protected TreeVisitor, ExecutionContext> getSingleSourceApplicableTest() {
+ // FIXME: (jaxrs) Test in visitor
+ return new UsesType<>(annotationTypeToRemove, true);
+ }
+
+ @Override
+ public @NotNull RemoveAnnotationIfAccompaniedVisitor getVisitor() {
+ return new RemoveAnnotationIfAccompaniedVisitor(annotationTypeToRemove, additionalAnnotationType);
+ }
+}
\ No newline at end of file
diff --git a/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/jaxrs/recipes/ReplaceMediaType.java b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/jaxrs/recipes/ReplaceMediaType.java
new file mode 100644
index 000000000..388706e56
--- /dev/null
+++ b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/jaxrs/recipes/ReplaceMediaType.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2021 - 2023 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.springframework.sbm.jee.jaxrs.recipes;
+
+import org.openrewrite.NlsRewrite;
+import org.openrewrite.Recipe;
+import org.openrewrite.java.ChangeType;
+import org.openrewrite.java.JavaParser;
+import org.openrewrite.java.JavaTemplate;
+import org.openrewrite.java.tree.Expression;
+import org.openrewrite.java.tree.J;
+import org.openrewrite.java.tree.JavaType;
+import org.openrewrite.java.tree.TypeUtils;
+import org.springframework.sbm.java.migration.recipes.RewriteConstructorInvocation;
+import org.springframework.sbm.java.migration.recipes.RewriteMethodInvocation;
+import org.springframework.sbm.java.migration.recipes.openrewrite.ReplaceConstantWithAnotherConstant;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+
+import static org.springframework.sbm.java.migration.recipes.RewriteConstructorInvocation.constructorMatcher;
+
+public class ReplaceMediaType extends Recipe {
+
+ @Override
+ public List getRecipeList() {
+
+ List recipeList = new ArrayList<>();
+
+ // Constants
+ Map mappings = new HashMap<>();
+ mappings.put("APPLICATION_ATOM_XML", "APPLICATION_ATOM_XML_VALUE");
+ mappings.put("APPLICATION_ATOM_XML_TYPE", "APPLICATION_ATOM_XML");
+
+ mappings.put("APPLICATION_FORM_URLENCODED", "APPLICATION_FORM_URLENCODED_VALUE");
+ mappings.put("APPLICATION_FORM_URLENCODED_TYPE", "APPLICATION_FORM_URLENCODED");
+
+ mappings.put("APPLICATION_JSON", "APPLICATION_JSON_VALUE");
+ mappings.put("APPLICATION_JSON_TYPE", "APPLICATION_JSON");
+
+ mappings.put("APPLICATION_JSON_PATCH_JSON", "APPLICATION_JSON_PATCH_JSON_VALUE");
+ mappings.put("APPLICATION_JSON_PATCH_JSON_TYPE", "APPLICATION_JSON_PATCH_JSON");
+
+ mappings.put("APPLICATION_OCTET_STREAM", "APPLICATION_OCTET_STREAM_VALUE");
+ mappings.put("APPLICATION_OCTET_STREAM_TYPE", "APPLICATION_OCTET_STREAM");
+
+ mappings.put("APPLICATION_SVG_XML", "APPLICATION_SVG_XML_VALUE");
+ mappings.put("APPLICATION_SVG_XML_TYPE", "APPLICATION_SVG_XML");
+
+ mappings.put("APPLICATION_XHTML_XML", "APPLICATION_XHTML_XML_VALUE");
+ mappings.put("APPLICATION_XHTML_XML_TYPE", "APPLICATION_XHTML_XML");
+
+ mappings.put("APPLICATION_XML", "APPLICATION_XML_VALUE");
+ mappings.put("APPLICATION_XML_TYPE", "APPLICATION_XML");
+
+ mappings.put("MULTIPART_FORM_DATA", "MULTIPART_FORM_DATA_VALUE");
+ mappings.put("MULTIPART_FORM_DATA_TYPE", "MULTIPART_FORM_DATA");
+
+ mappings.put("SERVER_SENT_EVENTS", "TEXT_EVENT_STREAM_VALUE");
+ mappings.put("SERVER_SENT_EVENTS_TYPE", "TEXT_EVENT_STREAM");
+
+ mappings.put("TEXT_HTML", "TEXT_HTML_VALUE");
+ mappings.put("TEXT_HTML_TYPE", "TEXT_HTML");
+
+ mappings.put("TEXT_PLAIN", "TEXT_PLAIN_VALUE");
+ mappings.put("TEXT_PLAIN_TYPE", "TEXT_PLAIN");
+
+ mappings.put("TEXT_XML", "TEXT_XML_VALUE");
+ mappings.put("TEXT_XML_TYPE", "TEXT_XML");
+
+ mappings.put("WILDCARD", "ALL_VALUE");
+ mappings.put("WILDCARD_TYPE", "ALL");
+
+ mappings.forEach(
+ (key, value) -> recipeList.add(new ReplaceConstantWithAnotherConstant("javax.ws.rs.core.MediaType." + key,"org.springframework.http.MediaType." + value))
+ );
+
+ recipeList.add(new ReplaceConstantWithAnotherConstant("javax.ws.rs.core.MediaType.CHARSET_PARAMETER","org.springframework.util.MimeType.PARAM_CHARSET"));
+ recipeList.add(new ReplaceConstantWithAnotherConstant("javax.ws.rs.core.MediaType.MEDIA_TYPE_WILDCARD","org.springframework.util.MimeType.WILDCARD_TYPE"));
+
+ // instance methods
+ // #isCompatible(MediaType)
+ recipeList.add(new RewriteMethodInvocation(RewriteMethodInvocation.methodInvocationMatcher("javax.ws.rs.core.MediaType isCompatible(javax.ws.rs.core.MediaType)"), (v, m, addImport) -> {
+ JavaType type = JavaType.buildType("org.springframework.http.MediaType");
+
+ J.Identifier newMethodName = m.getName().withSimpleName("isCompatibleWith");
+ Expression newSelect = m.getSelect().withType(type);
+ JavaType.Method newMethodType = m.getMethodType().withReturnType(type).withDeclaringType(TypeUtils.asFullyQualified(type));
+ List newMethodArguments = List.of(m.getArguments().get(0).withType(type));
+
+ return m
+ .withName(newMethodName)
+ .withSelect(newSelect)
+ .withMethodType(newMethodType)
+ .withArguments(newMethodArguments);
+ }));
+
+ // #withCharset(String)
+ recipeList.add(new RewriteMethodInvocation(RewriteMethodInvocation.methodInvocationMatcher("javax.ws.rs.core.MediaType withCharset(java.lang.String)"), (v, m, addImport) -> {
+ JavaTemplate template = JavaTemplate.builder("new MediaType(#{any(org.springframework.http.MediaType)}, Charset.forName(#{any(java.lang.String)}))")
+ .imports("org.springframework.http.MediaType", "java.nio.charset.Charset")
+ .build();
+ addImport.accept("java.nio.charset.Charset");
+ addImport.accept("org.springframework.http.MediaType");
+
+ return template.apply(v.getCursor(), m.getCoordinates().replace(), m.getSelect(), m.getArguments().get(0));
+ }));
+
+ // #getParameters() - comes with org.springframework.util.MimeType#getParameters()
+ // #getSubtype() - comes with org.springframework.util.MimeType#getSubtype()
+ // #getType() - comes with org.springframework.util.MimeType#getType()
+ // #isWildcardSubtype() - comes with org.springframework.util.MimeType#isWildcardSubtype()
+ // #isWildcardType() - comes with org.springframework.util.MimeType#isWildcardType()
+
+ // static methods
+
+ // #valueOf(String) present on Spring MediaType
+
+ // constructors
+
+ // MediaType() -> new MediaType(MimeType.WILDCARD_TYPE, MimeType.WILDCARD_TYPE)
+ recipeList.add(new RewriteConstructorInvocation(constructorMatcher("javax.ws.rs.core.MediaType"), (v, m, addImport) -> {
+ JavaTemplate template = JavaTemplate.builder("new MediaType(MimeType.WILDCARD_TYPE, MimeType.WILDCARD_TYPE)")
+ .imports("org.springframework.http.MediaType", "org.springframework.util.MimeType")
+ .build();
+ addImport.accept("org.springframework.util.MimeType");
+ addImport.accept("org.springframework.http.MediaType");
+
+ return template.apply(v.getCursor(), m.getCoordinates().replace());
+ }));
+
+ // MediaType(String, String) - present on Spring MediaType
+ recipeList.add(new RewriteConstructorInvocation(constructorMatcher("javax.ws.rs.core.MediaType", "java.lang.String", "java.lang.String"), (v, m, addImport) -> {
+ JavaType type = JavaType.buildType("org.springframework.http.MediaType");
+ return m.withConstructorType(m.getConstructorType().withDeclaringType(TypeUtils.asFullyQualified(type)));
+ }));
+
+ // MediaType(String, String, String) -> MediaType(String, String, Charset)
+ recipeList.add(new RewriteConstructorInvocation(constructorMatcher("javax.ws.rs.core.MediaType", "java.lang.String", "java.lang.String", "java.lang.String"), (v, m, addImport) -> {
+ List arguments = m.getArguments();
+ JavaTemplate template = JavaTemplate.builder("new MediaType(#{any(java.lang.String)}, #{any(java.lang.String)}, Charset.forName(#{any(java.lang.String)}))")
+ .imports("org.springframework.http.MediaType", "java.nio.charset.Charset")
+ .build();
+ addImport.accept("java.nio.charset.Charset");
+ addImport.accept("org.springframework.http.MediaType");
+
+ return template.apply(v.getCursor(), m.getCoordinates().replace(), arguments.get(0), arguments.get(1), arguments.get(2));
+ }));
+
+ // MediaType(String, String, Map) - present on Spring MediaType
+ recipeList.add(new RewriteConstructorInvocation(constructorMatcher("javax.ws.rs.core.MediaType", "java.lang.String", "java.lang.String", "java.util.Map"), (v, m, addImport) -> {
+ JavaType type = JavaType.buildType("org.springframework.http.MediaType");
+ return m.withConstructorType(m.getConstructorType().withDeclaringType(TypeUtils.asFullyQualified(type)));
+ }));
+
+ // Type references
+ recipeList.add(new ChangeType("javax.ws.rs.core.MediaType", "org.springframework.http.MediaType", false));
+
+ return recipeList;
+ }
+
+ @Override
+ public String getDisplayName() {
+ return "Replace JAX-RS MediaType with Spring MediaType";
+ }
+
+ @Override
+ public @NlsRewrite.Description String getDescription() {
+ return getDisplayName();
+ }
+
+}
diff --git a/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/jaxrs/recipes/ReplaceRequestParameterProperties.java b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/jaxrs/recipes/ReplaceRequestParameterProperties.java
new file mode 100644
index 000000000..0f0ffa5af
--- /dev/null
+++ b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/jaxrs/recipes/ReplaceRequestParameterProperties.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2021 - 2023 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.springframework.sbm.jee.jaxrs.recipes;
+
+import org.jetbrains.annotations.NotNull;
+import org.openrewrite.NlsRewrite;
+import org.openrewrite.Recipe;
+import org.openrewrite.config.RecipeExample;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Vincent Botteman
+ */
+public class ReplaceRequestParameterProperties extends Recipe {
+
+ @Override
+ public @NotNull String getDisplayName() {
+ return "Migrate the properties of a request parameter: default value, ...";
+ }
+
+ @Override
+ public @NlsRewrite.Description String getDescription() {
+ return getDisplayName();
+ }
+
+ @Override
+ public List getRecipeList() {
+ List recipeList = new ArrayList<>();
+ recipeList.add(new CopyAnnotationAttribute(
+ "javax.ws.rs.DefaultValue", "value", "org.springframework.web.bind.annotation.RequestParam", "defaultValue")
+ );
+ recipeList.add(new RemoveAnnotationIfAccompanied(
+ "javax.ws.rs.DefaultValue", "org.springframework.web.bind.annotation.RequestParam"
+ ));
+ return recipeList;
+ }
+}
\ No newline at end of file
diff --git a/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/jaxrs/recipes/ReplaceResponseEntityBuilder.java b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/jaxrs/recipes/ReplaceResponseEntityBuilder.java
new file mode 100644
index 000000000..86b993db3
--- /dev/null
+++ b/components/jaxrs-recipes/src/main/java/org/springframework/sbm/jee/jaxrs/recipes/ReplaceResponseEntityBuilder.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright 2021 - 2023 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.springframework.sbm.jee.jaxrs.recipes;
+
+import org.openrewrite.NlsRewrite;
+import org.openrewrite.Recipe;
+import org.openrewrite.Tree;
+import org.openrewrite.java.ChangeType;
+import org.openrewrite.java.JavaTemplate;
+import org.openrewrite.java.JavaTemplate.Builder;
+import org.openrewrite.java.MethodMatcher;
+import org.springframework.sbm.java.migration.recipes.RewriteMethodInvocation;
+import org.springframework.sbm.java.migration.visitor.VisitorUtils;
+import org.springframework.sbm.java.migration.visitor.VisitorUtils.AdjustTypesFromExpressionMarkers;
+import org.springframework.sbm.java.migration.visitor.VisitorUtils.MarkReturnType;
+import org.springframework.sbm.java.migration.visitor.VisitorUtils.MarkWithTemplate;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.springframework.sbm.java.migration.recipes.RewriteMethodInvocation.methodInvocationMatcher;
+import static org.springframework.sbm.java.migration.recipes.RewriteMethodInvocation.renameMethodInvocation;
+
+public class ReplaceResponseEntityBuilder extends Recipe {
+
+ @Override
+ public List getRecipeList() {
+ List recipeList = new ArrayList<>();
+
+ // #allow(String...)
+ recipeList.add(new RewriteMethodInvocation(
+ RewriteMethodInvocation.methodInvocationMatcher("javax.ws.rs.core.Response.ResponseBuilder allow(java.lang.String...)"),
+ (v, m, addImport) -> {
+ String transformedArgs = m.getArguments().stream().map(arg -> "HttpMethod.resolve(#{any()})").collect(Collectors.joining(", "));
+ JavaTemplate t = JavaTemplate.builder("#{any(org.springframework.http.ResponseEntity.HeadersBuilder)}.allow(" + transformedArgs + ")").imports("org.springframework.http.HttpMethod", "org.springframework.http.ResponseEntity.HeadersBuilder").build();
+// v.maybeAddImport("org.springframework.http.HttpMethod");
+ addImport.accept("org.springframework.http.HttpMethod");
+ List