Skip to content

Commit

Permalink
Spring Batch recipe for MigrateStepBuilderFactory (#284)
Browse files Browse the repository at this point in the history
* springbatch recipe configuration

* new spring-batch 5.x migration recipes

* remove trace

* add MigrateItemWriterWrite recipe

* Tabs to spaces

* Get MigrateItemWriterWrite tests passing

* Preserve annotations, add @OverRide if missing

* add missing MigrateItemWriterWrite

* MigrateStepBuilderFactory recipe draft

* Update to use Rewrite 8

* Apply automated suggestions

* Apply suggestions from code review

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Pull forward method declaration change

* Fix missing types on original method arguments

* Split visitors to fix first of the tests

* Fixes around empty parameters

* Finishing touches

* Make `MigrateStepBuilderFactory` part of Batch 5.0

---------

Co-authored-by: Sam Snyder <[email protected]>
Co-authored-by: Tim te Beek <[email protected]>
Co-authored-by: Tim te Beek <[email protected]>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
5 people authored Aug 11, 2024
1 parent 5c7ad98 commit 3706324
Show file tree
Hide file tree
Showing 3 changed files with 386 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* Copyright 2023 the original author or authors.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openrewrite.java.spring.batch;

import org.openrewrite.*;
import org.openrewrite.internal.ListUtils;
import org.openrewrite.internal.lang.Nullable;
import org.openrewrite.java.*;
import org.openrewrite.java.search.FindMethods;
import org.openrewrite.java.search.UsesMethod;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.Space;
import org.openrewrite.java.tree.Statement;
import org.openrewrite.java.tree.TypeUtils;

import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

public class MigrateStepBuilderFactory extends Recipe {

private static final String STEP_BUILDER_FACTORY_GET = "org.springframework.batch.core.configuration.annotation.StepBuilderFactory get(java.lang.String)";

@Override
public String getDisplayName() {
return "Migrate `StepBuilderFactory` to `StepBuilder`";
}

@Override
public String getDescription() {
return "`StepBuilderFactory` was deprecated in spring-batch 5.x. It is replaced by `StepBuilder`.";
}

@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return Preconditions.check(new UsesMethod<>(STEP_BUILDER_FACTORY_GET),
new JavaVisitor<ExecutionContext>() {
@Override
public J visit(@Nullable Tree tree, ExecutionContext ctx) {
tree = new AddJobRepositoryVisitor().visit(tree, ctx);
return new NewStepBuilderVisitor().visit(tree, ctx);
}
}
);
}

private static class AddJobRepositoryVisitor extends JavaIsoVisitor<ExecutionContext> {
@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDeclaration, ExecutionContext ctx) {
J.ClassDeclaration cd = super.visitClassDeclaration(classDeclaration, ctx);

// Remove StepBuilderFactory field if StepBuilderFactory.get(..) is used further down
if (!FindMethods.find(classDeclaration, STEP_BUILDER_FACTORY_GET).isEmpty()) {
cd = cd.withBody(cd.getBody().withStatements(ListUtils.map(cd.getBody().getStatements(), statement -> {
if (statement instanceof J.VariableDeclarations
&& ((J.VariableDeclarations) statement).getTypeExpression() != null) {
if (TypeUtils.isOfClassType(((J.VariableDeclarations) statement).getTypeExpression().getType(),
"org.springframework.batch.core.configuration.annotation.StepBuilderFactory")) {
return null;
}
}
return statement;
})));
}

return cd;
}

@Override
public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration md, ExecutionContext ctx) {
// Add JobRepository parameter to method if StepBuilderFactory.get(..) is used further down
if (!FindMethods.find(md, STEP_BUILDER_FACTORY_GET).isEmpty()) {
List<Object> params = md.getParameters().stream()
.filter(j -> !(j instanceof J.Empty) && !isJobBuilderFactoryParameter(j))
.collect(Collectors.toList());

if (params.isEmpty() && md.isConstructor()) {
//noinspection DataFlowIssue
return null;
}

if (md.getParameters().stream().noneMatch(this::isJobRepositoryParameter) && !md.isConstructor()) {
maybeAddImport("org.springframework.batch.core.repository.JobRepository");
boolean parametersEmpty = md.getParameters().isEmpty() || md.getParameters().get(0) instanceof J.Empty;
J.VariableDeclarations vdd = JavaTemplate.builder("JobRepository jobRepository")
.contextSensitive()
.imports("org.springframework.batch.core.repository.JobRepository")
.javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "spring-batch-core-5.+"))
.build()
.<J.MethodDeclaration>apply(getCursor(), md.getCoordinates().replaceParameters())
.getParameters().get(0).withPrefix(parametersEmpty ? Space.EMPTY : Space.SINGLE_SPACE);
if (parametersEmpty) {
md = md.withParameters(Collections.singletonList(vdd))
.withMethodType(md.getMethodType()
.withParameterTypes(Collections.singletonList(vdd.getType())));
} else {
md = md.withParameters(ListUtils.concat(md.getParameters(), vdd))
.withMethodType(md.getMethodType()
.withParameterTypes(ListUtils.concat(md.getMethodType().getParameterTypes(), vdd.getType())));
}
}
}

return super.visitMethodDeclaration(md, ctx);
}

private boolean isJobRepositoryParameter(Statement statement) {
return statement instanceof J.VariableDeclarations
&& TypeUtils.isOfClassType(((J.VariableDeclarations) statement).getType(),
"org.springframework.batch.core.repository.JobRepository");
}

private boolean isJobBuilderFactoryParameter(Statement statement) {
return statement instanceof J.VariableDeclarations
&& TypeUtils.isOfClassType(((J.VariableDeclarations) statement).getType(),
"org.springframework.batch.core.configuration.annotation.StepBuilderFactory");
}
}

private static class NewStepBuilderVisitor extends JavaVisitor<ExecutionContext> {
final MethodMatcher STEP_BUILDER_FACTORY_MATCHER = new MethodMatcher(STEP_BUILDER_FACTORY_GET);

@Override
public J visitMethodInvocation(J.MethodInvocation mi, ExecutionContext ctx) {
if (STEP_BUILDER_FACTORY_MATCHER.matches(mi)) {
maybeAddImport("org.springframework.batch.core.step.builder.StepBuilder", false);
maybeRemoveImport("org.springframework.beans.factory.annotation.Autowired");
maybeRemoveImport("org.springframework.batch.core.configuration.annotation.StepBuilderFactory");
return JavaTemplate.builder("new StepBuilder(#{any(java.lang.String)}, jobRepository)")
.contextSensitive()
.javaParser(JavaParser.fromJavaVersion()
.classpathFromResources(ctx, "spring-batch-core-5.+", "spring-batch-infrastructure-5.+"))
.imports("org.springframework.batch.core.step.builder.StepBuilder")
.build()
.apply(getCursor(), mi.getCoordinates().replace(), mi.getArguments().get(0));
}
return super.visitMethodInvocation(mi, ctx);
}
}
}
1 change: 1 addition & 0 deletions src/main/resources/META-INF/rewrite/spring-batch-5.0.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ recipeList:
- org.openrewrite.java.spring.batch.ImplementChunkListenerDirectly
- org.openrewrite.java.spring.batch.ImplementSkipListenerSupportDirectly
- org.openrewrite.java.spring.batch.MigrateJobBuilderFactory
- org.openrewrite.java.spring.batch.MigrateStepBuilderFactory
- org.openrewrite.java.spring.batch.MigrateItemWriterWrite
- org.openrewrite.java.spring.batch.RemoveDefaultBatchConfigurer
- org.openrewrite.java.ChangeType:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
/*
* Copyright 2023 the original author or authors.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openrewrite.java.spring.batch;

import org.junit.jupiter.api.Test;
import org.openrewrite.DocumentExample;
import org.openrewrite.InMemoryExecutionContext;
import org.openrewrite.java.JavaParser;
import org.openrewrite.test.RecipeSpec;
import org.openrewrite.test.RewriteTest;

import static org.openrewrite.java.Assertions.java;

class MigrateStepBuilderFactoryTest implements RewriteTest {

@Override
public void defaults(RecipeSpec spec) {
spec.recipe(new MigrateStepBuilderFactory())
.parser(JavaParser.fromJavaVersion().classpathFromResources(new InMemoryExecutionContext(),
"spring-batch-core-4.3.+",
"spring-batch-infrastructure-4.3.+",
"spring-beans-4.3.30.RELEASE",
"spring-context-4.3.30.RELEASE"
));
}

@DocumentExample
@Test
void replaceStepBuilderFactoryWithTasket() {
// language=java
rewriteRun(
java(
"""
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
class MyJobConfig {
@Autowired
private StepBuilderFactory stepBuilderFactory;
@Bean
Step myStep(Tasklet myTasklet) {
return this.stepBuilderFactory.get("myStep")
.tasklet(myTasklet)
.build();
}
}
""",
"""
import org.springframework.batch.core.Step;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.context.annotation.Bean;
class MyJobConfig {
@Bean
Step myStep(Tasklet myTasklet, JobRepository jobRepository) {
return new StepBuilder("myStep", jobRepository)
.tasklet(myTasklet)
.build();
}
}
"""
)
);
}

@Test
void replaceStepBuilderFactoryWithChunk() {
// language=java
rewriteRun(
java(
"""
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
class MyJobConfig {
@Autowired
private StepBuilderFactory stepBuilderFactory;
@Bean
Step myStep() {
return this.stepBuilderFactory.get("myStep")
.<String, String> chunk(10)
.reader(reader())
.writer(writer())
.build();
}
private ItemWriter<String> writer() {
return null;
}
private ItemReader<String> reader() {
return null;
}
}
""",
"""
import org.springframework.batch.core.Step;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.context.annotation.Bean;
class MyJobConfig {
@Bean
Step myStep(JobRepository jobRepository) {
return new StepBuilder("myStep", jobRepository)
.<String, String> chunk(10)
.reader(reader())
.writer(writer())
.build();
}
private ItemWriter<String> writer() {
return null;
}
private ItemReader<String> reader() {
return null;
}
}
"""
)
);
}

@Test
void replaceStepBuilderFactoryWithCompletionPolicyChunk() {
// language=java
rewriteRun(
java(
"""
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.repeat.CompletionPolicy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
class MyJobConfig {
@Autowired
private StepBuilderFactory stepBuilderFactory;
@Bean
Step myStep() {
return this.stepBuilderFactory.get("myStep")
.<String, String> chunk(completionPolicy())
.reader(reader())
.writer(writer())
.build();
}
private CompletionPolicy completionPolicy() {
return null;
}
private ItemWriter<String> writer() {
return null;
}
private ItemReader<String> reader() {
return null;
}
}
""",
"""
import org.springframework.batch.core.Step;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.repeat.CompletionPolicy;
import org.springframework.context.annotation.Bean;
class MyJobConfig {
@Bean
Step myStep(JobRepository jobRepository) {
return new StepBuilder("myStep", jobRepository)
.<String, String> chunk(completionPolicy())
.reader(reader())
.writer(writer())
.build();
}
private CompletionPolicy completionPolicy() {
return null;
}
private ItemWriter<String> writer() {
return null;
}
private ItemReader<String> reader() {
return null;
}
}
"""
)
);
}
}

0 comments on commit 3706324

Please sign in to comment.