diff --git a/build.gradle.kts b/build.gradle.kts index 597622de2..ad735e2c4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -120,8 +120,8 @@ dependencies { testImplementation("com.github.marschall:memoryfilesystem:latest.release") // for generating properties migration configurations - testImplementation("com.fasterxml.jackson.core:jackson-databind:latest.release") - testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:latest.release") + testImplementation("com.fasterxml.jackson.core:jackson-databind:2.12.+") + testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.12.+") testImplementation("io.github.classgraph:classgraph:latest.release") testRuntimeOnly("org.openrewrite:rewrite-java-11:${rewriteVersion}") diff --git a/tmp/ComponentToBeanConfiguration.java b/tmp/ComponentToBeanConfiguration.java deleted file mode 100644 index bc9c0580f..000000000 --- a/tmp/ComponentToBeanConfiguration.java +++ /dev/null @@ -1,247 +0,0 @@ -/* - * Copyright 2020 the original author or authors. - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * https://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.openrewrite.java.spring; - -import org.openrewrite.AutoConfigure; -import org.openrewrite.Validated; -import org.openrewrite.internal.lang.Nullable; -import org.openrewrite.java.AutoFormat; -import org.openrewrite.java.JavaRefactorVisitor; -import org.openrewrite.java.tree.J; -import org.openrewrite.java.tree.JavaType; -import org.openrewrite.java.tree.TypeUtils; - -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static java.util.Collections.emptyList; -import static java.util.stream.Collectors.toList; -import static org.openrewrite.Formatting.formatFirstPrefix; -import static org.openrewrite.Validated.required; - -/** - * Generates a new {@code @Configuration} class (or updates an existing one) with {@code @Bean} definitions for all - * {@code @Component} annotated types and places it in the same package as the main {@code @SpringBootApplication}. - */ -// FIXME generator! -@AutoConfigure -public class ComponentToBeanConfiguration extends JavaRefactorVisitor { - private String configurationClass; - - @Nullable - private J.CompilationUnit existingConfiguration; - - @Nullable - private J.CompilationUnit springBootApplication; - - private final Set componentsToCreateBeansDefinitionsFor = new TreeSet<>( - Comparator.comparing(J.ClassDecl::getSimpleName)); - - public ComponentToBeanConfiguration() { - setCursoringOn(); - } - - @Override - public Validated validate() { - return required("configurationClass", configurationClass); - } - - public void setConfigurationClass(String configurationClass) { - this.configurationClass = configurationClass; - } - -// @Override -// public J.CompilationUnit getGenerated() { -// if (existingConfiguration == null && springBootApplication == null) { -// // TODO how do we warn about this, don't know where to put new configuration class? -// return null; -// } -// -// return Optional.ofNullable(existingConfiguration) -// .orElseGet(() -> { -// Path sourceSet = Paths.get(springBootApplication.getSourcePath()) -// .getParent(); -// -// String pkg = springBootApplication.getPackageDecl().getExpr().printTrimmed(); -// for (int i = 0; i < pkg.chars().filter(n -> n == '.').count(); i++) { -// sourceSet = sourceSet.getParent(); -// } -// -// J.CompilationUnit configurationClass = J.CompilationUnit.buildEmptyClass(sourceSet, pkg, this.configurationClass); -// return configurationClass.refactor() -// .visit(new AddAnnotation.Scoped(configurationClass.getClasses().get(0), -// "org.springframework.context.annotation.Configuration")) -// .fix(1).getFixed(); -// }).refactor().visit(new AddBeanDefinitions()).fix().getFixed(); -// } - - private class AddBeanDefinitions extends JavaRefactorVisitor { - public AddBeanDefinitions() { - setCursoringOn(); - } - - @Override - public boolean isIdempotent() { - return false; - } - - @Override - public J visitClassDecl(J.ClassDecl classDecl) { - J.ClassDecl configClass = refactor(classDecl, super::visitClassDecl); - if (!configClass.findAnnotationsOnClass("@org.springframework.context.annotation.Configuration").isEmpty()) { - J.CompilationUnit cu = getCursor().firstEnclosing(J.CompilationUnit.class); - assert cu != null; - - for (J.ClassDecl component : componentsToCreateBeansDefinitionsFor) { - JavaType.Class componentType = TypeUtils.asClass(component.getType()); - if (componentType == null) { - // TODO how to report that we can't generate a bean definition - continue; - } - - maybeAddImport(componentType); - - List fieldCollaborators = autowiredFields(component); - List constructorCollaborators = constructorInjectedFields(component); - - List paramTypes = emptyList(); - String constructorArgs = ""; - String params = ""; - if (!fieldCollaborators.isEmpty()) { - params = fieldCollaborators.stream() - .map(param -> param.getTypeExpr().printTrimmed() + " " + param.getVars().iterator().next().printTrimmed()) - .collect(Collectors.joining(", ")); - paramTypes = fieldCollaborators.stream() - .map(J.VariableDecls::getTypeAsClass) - .collect(toList()); - } else if (!constructorCollaborators.isEmpty()) { - params = constructorCollaborators.stream() - .map(param -> param.getTypeExpr().printTrimmed() + " " + param.getVars().iterator().next().printTrimmed()) - .collect(Collectors.joining(", ")); - paramTypes = constructorCollaborators.stream() - .map(J.VariableDecls::getTypeAsClass) - .collect(toList()); - constructorArgs = constructorCollaborators.stream() - .flatMap(param -> param.getVars().stream()) - .map(J.VariableDecls.NamedVar::getSimpleName) - .collect(Collectors.joining(", ")); - } - - String className = componentType.getClassName(); - String lowerClassName = Character.toLowerCase(className.charAt(0)) + className.substring(1); - - String beanDefinitionSource = "@Bean\n" + - className + " " + lowerClassName + "(" + params + ") {\n"; - - if (!fieldCollaborators.isEmpty()) { - beanDefinitionSource += className + " " + lowerClassName + " = new " + className + "();\n"; - for (J.VariableDecls fieldCollaborator : fieldCollaborators) { - beanDefinitionSource += fieldCollaborator.getVars().stream().findAny() - .map(field -> { - String lowerName = field.getSimpleName(); - return lowerClassName + ".set" + Character.toUpperCase(lowerName.charAt(0)) + lowerName.substring(1) + - "(" + lowerName + ");\n"; - }) - .orElse(""); - } - beanDefinitionSource += "return " + lowerClassName + ";\n"; - } else { - beanDefinitionSource += "return new " + className + "(" + constructorArgs + ");\n"; - } - - beanDefinitionSource += "}"; - - J.MethodDecl beanDefinition = treeBuilder.buildMethodDeclaration( - configClass, - beanDefinitionSource, - Stream.concat( - Stream.of( - JavaType.Class.build("org.springframework.context.annotation.Bean"), - componentType - ), - paramTypes.stream() - ).toArray(JavaType.Class[]::new) - ); - - andThen(new AutoFormat(beanDefinition)); - - List statements = new ArrayList<>(configClass.getBody().getStatements()); - statements.add(beanDefinition.withPrefix("\n" + beanDefinition.getPrefix())); - - configClass = configClass.withBody(configClass.getBody().withStatements(statements)); - } - } - - return configClass; - } - - private List autowiredFields(J.ClassDecl component) { - return component.getFields().stream() - .filter(f -> f.getAnnotations().stream() - .anyMatch(ann -> - TypeUtils.hasElementType(ann.getType(), "org.springframework.beans.factory.annotation.Autowired") || - TypeUtils.hasElementType(ann.getType(), "javax.inject.Inject") - ) - ) - .collect(toList()); - } - - private List constructorInjectedFields(J.ClassDecl component) { - return component.getMethods().stream() - .filter(J.MethodDecl::isConstructor) - .max(Comparator.comparing(m -> m.getParams().getParams().size())) - .map(m -> m.getParams().getParams().stream() - .filter(J.VariableDecls.class::isInstance) - .map(J.VariableDecls.class::cast) - .collect(toList()) - ) - .orElse(emptyList()); - } - } - - @Override - public J visitClassDecl(J.ClassDecl classDecl) { - J.ClassDecl c = refactor(classDecl, super::visitClassDecl); - - JavaType.Class classType = TypeUtils.asClass(classDecl.getType()); - if (classType != null && classType.getFullyQualifiedName().equals(configurationClass)) { - existingConfiguration = getCursor().firstEnclosing(J.CompilationUnit.class); - return c; - } - - if (!classDecl.findAnnotationsOnClass("@org.springframework.boot.autoconfigure.SpringBootApplication").isEmpty()) { - springBootApplication = getCursor().firstEnclosing(J.CompilationUnit.class); - } - - List components = Stream.concat( - classDecl.findAnnotationsOnClass("@org.springframework.stereotype.Component").stream(), - Stream.concat( - classDecl.findAnnotationsOnClass("@org.springframework.stereotype.Repository").stream(), - classDecl.findAnnotationsOnClass("@org.springframework.stereotype.Service").stream() - ) - ).collect(toList()); - - if (!components.isEmpty()) { - List annotations = new ArrayList<>(c.getAnnotations()); - annotations.removeAll(components); - c = c.withAnnotations(formatFirstPrefix(annotations, c.getAnnotations().get(0).getFormatting().getPrefix())); - componentsToCreateBeansDefinitionsFor.add(c); - } - - return c; - } -} diff --git a/tmp/ComponentToBeanConfigurationTest.kt b/tmp/ComponentToBeanConfigurationTest.kt deleted file mode 100644 index d014985bf..000000000 --- a/tmp/ComponentToBeanConfigurationTest.kt +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright 2020 the original author or authors. - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * https://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.openrewrite.java.spring - -import org.openrewrite.RefactorVisitor -import org.openrewrite.RefactorVisitorTestForParser -import org.openrewrite.java.JavaParser -import org.openrewrite.java.tree.J -import java.nio.file.Path - -class ComponentToBeanConfigurationTest( - override val parser: JavaParser = JavaParser.fromJavaVersion() - .classpath("spring-boot-autoconfigure", "spring-beans", "spring-context") - .build(), - override val visitors: Iterable> = listOf( - ComponentToBeanConfiguration().apply { - setConfigurationClass("MyConfiguration") - } - ) -) : RefactorVisitorTestForParser { - - private val app = "io/moderne/app/MyApplication" to """ - package io.moderne.app; - - import org.springframework.boot.autoconfigure.SpringBootApplication; - - @SpringBootApplication - public class MyApplication { - } - """.trimIndent() - - private val component = "io/moderne/app/components/MyComponent" to """ - package io.moderne.app.components; - - import org.springframework.stereotype.Component; - - @Component - public class MyComponent { - } - """.trimIndent() - - private val repository = "io/moderne/app/repositories/MyRepository" to """ - package io.moderne.app.repositories; - - import org.springframework.stereotype.Repository; - - @Repository - public class MyRepository { - } - """.trimIndent() - - private val service = "io/moderne/app/services/MyService" to """ - package io.moderne.app.services; - - import io.moderne.app.repositories.MyRepository; - import org.springframework.stereotype.Service; - - @Service - public class MyService { - private final MyRepository repo; - - public MyService(MyRepository repo) { - this.repo = repo; - } - } - """.trimIndent() - -// @Test -// fun beanWithNoCollaborators(@TempDir tempDir: Path) { -// parse(tempDir, app, component).map { cu -> -// assertThat(cu.refactor().visit(visitor).fix().fixed.printTrimmed()).doesNotContain("@Component") -// } -// -// val generated = visitor.generated -// -// assertThat(generated).isNotNull() -// -// assertRefactored(generated!!, """ -// package io.moderne.app; -// -// import io.moderne.app.components.MyComponent; -// import org.springframework.context.annotation.Configuration; -// -// @Configuration -// public class MyConfiguration { -// -// @Bean -// MyComponent myComponent() { -// return new MyComponent(); -// } -// } -// """) -// } - -// @Test -// fun beanWithConstructorInjectableCollaborators(@TempDir tempDir: Path) { -// parse(tempDir, app, repository, service).map { cu -> -// assertThat(cu.refactor().visit(visitor).fix().fixed.printTrimmed()).doesNotContain("@Component") -// } -// -// val generated = visitor.generated -// -// assertThat(generated).isNotNull() -// -// assertRefactored(generated!!, """ -// package io.moderne.app; -// -// import io.moderne.app.repositories.MyRepository; -// import io.moderne.app.services.MyService; -// import org.springframework.context.annotation.Configuration; -// -// @Configuration -// public class MyConfiguration { -// -// @Bean -// MyRepository myRepository() { -// return new MyRepository(); -// } -// -// @Bean -// MyService myService(MyRepository repo) { -// return new MyService(repo); -// } -// } -// """) -// } -// -// @Test -// fun beanWithFieldInjectedCollaborators(@TempDir tempDir: Path) { -// val serviceFieldInjectable = "io/moderne/app/services/MyService" to """ -// package io.moderne.app.services; -// -// import io.moderne.app.repositories.MyRepository; -// import org.springframework.beans.factory.annotation.Autowired; -// import org.springframework.stereotype.Service; -// -// @Service -// public class MyService { -// @Autowired -// private MyRepository repo; -// -// public void setRepo(MyRepository repo) { -// this.repo = repo; -// } -// } -// """.trimIndent() -// -// parse(tempDir, app, repository, serviceFieldInjectable).map { cu -> -// assertThat(cu.refactor().visit(visitor).fix().fixed.printTrimmed()).doesNotContain("@Component") -// } -// -// val generated = visitor.generated -// -// assertThat(generated).isNotNull() -// -// assertRefactored(generated!!, """ -// package io.moderne.app; -// -// import io.moderne.app.repositories.MyRepository; -// import io.moderne.app.services.MyService; -// import org.springframework.context.annotation.Configuration; -// -// @Configuration -// public class MyConfiguration { -// -// @Bean -// MyRepository myRepository() { -// return new MyRepository(); -// } -// -// @Bean -// MyService myService(MyRepository repo) { -// MyService myService = new MyService(); -// myService.setRepo(repo); -// return myService; -// } -// } -// """) -// } - - private fun parse(root: Path, vararg sources: Pair): List = parser.parse( - sources.map { s -> - val sourcePath = root.resolve(Path.of(s.first + ".java")) - sourcePath.parent.toFile().mkdirs() - sourcePath.toFile().writeText(s.second) - sourcePath - }, - null - ) -} diff --git a/tmp/ConstructorInjection.java b/tmp/ConstructorInjection.java deleted file mode 100644 index 1ed73d5e9..000000000 --- a/tmp/ConstructorInjection.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright 2020 the original author or authors. - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * https://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.openrewrite.java.spring; - -import org.openrewrite.AutoConfigure; -import org.openrewrite.java.AddAnnotation; -import org.openrewrite.java.GenerateConstructorUsingFields; -import org.openrewrite.java.JavaParser; -import org.openrewrite.java.JavaRefactorVisitor; -import org.openrewrite.java.tree.J; -import org.openrewrite.java.tree.JavaType; - -import java.util.List; -import java.util.Set; - -import static java.util.stream.Collectors.toList; -import static java.util.stream.Collectors.toSet; -import static org.openrewrite.Formatting.firstPrefix; -import static org.openrewrite.Formatting.formatFirstPrefix; -import static org.openrewrite.Tree.randomId; -import static org.openrewrite.java.tree.TypeUtils.isOfClassType; - -@AutoConfigure -public class ConstructorInjection extends JavaRefactorVisitor { - private final JavaParser javaParser = JavaParser.fromJavaVersion().build(); - - private boolean useLombokRequiredArgsAnnotation = false; - private boolean useJsr305Annotations = false; - - public void setUseJsr305Annotations(boolean useJsr305Annotations) { - this.useJsr305Annotations = useJsr305Annotations; - } - - public void setUseLombokRequiredArgsAnnotation(boolean useLombokRequiredArgsAnnotation) { - this.useLombokRequiredArgsAnnotation = useLombokRequiredArgsAnnotation; - } - - @Override - public J visitClassDecl(J.ClassDecl classDecl) { - J.ClassDecl cd = refactor(classDecl, super::visitClassDecl); - - if (cd.getFields().stream().anyMatch(this::isFieldInjected)) { - List statements = cd.getBody().getStatements().stream() - .map(stat -> stat.whenType(J.VariableDecls.class) - .filter(this::isFieldInjected) - .map(mv -> { - J.VariableDecls fixedField = mv - .withAnnotations(mv.getAnnotations().stream() - .filter(ann -> !isFieldInjectionAnnotation(ann) || - (useJsr305Annotations && ann.getArgs() != null && ann.getArgs().getArgs().stream() - .anyMatch(arg -> arg.whenType(J.Assign.class) - .map(assign -> ((J.Ident) assign.getVariable()).getSimpleName().equals("required")) - .orElse(false)))) - .map(ann -> { - if (isFieldInjectionAnnotation(ann)) { - maybeAddImport("javax.annotation.Nonnull"); - - JavaType.Class nonnullType = JavaType.Class.build("javax.annotation.Nonnull"); - return ann - .withAnnotationType(J.Ident.build(randomId(), "Nonnull", nonnullType, - ann.getAnnotationType().getFormatting())) - .withArgs(null) - .withType(nonnullType); - } - return ann; - }) - .collect(toList())) - .withModifiers("private", "final"); - - maybeRemoveImport("org.springframework.beans.factory.annotation.Autowired"); - - return (J) (fixedField.getAnnotations().isEmpty() && !mv.getAnnotations().isEmpty() ? - fixedField.withModifiers(formatFirstPrefix(fixedField.getModifiers(), - firstPrefix(mv.getAnnotations()))) : - fixedField); - }) - .orElse(stat)) - .collect(toList()); - - if (!hasRequiredArgsConstructor(cd)) { - andThen(useLombokRequiredArgsAnnotation ? - new AddAnnotation.Scoped(cd, "lombok.RequiredArgsConstructor") : - new GenerateConstructorUsingFields.Scoped(cd, getInjectedFields(cd))); - } - - List setterNames = getInjectedFields(cd).stream() - .map(mv -> { - String name = mv.getVars().get(0).getSimpleName(); - return "set" + name.substring(0, 1).toUpperCase() + name.substring(1); - }) - .collect(toList()); - - cd = cd.withBody(cd.getBody().withStatements(statements.stream() - .filter(stat -> stat.whenType(J.MethodDecl.class) - .map(md -> !setterNames.contains(md.getSimpleName())) - .orElse(true)) - .collect(toList()))); - } - - return cd; - } - - private boolean hasRequiredArgsConstructor(J.ClassDecl cd) { - Set injectedFieldNames = getInjectedFields(cd).stream().map(f -> f.getVars().get(0).getSimpleName()).collect(toSet()); - - return cd.getBody().getStatements().stream().anyMatch(stat -> stat.whenType(J.MethodDecl.class) - .filter(J.MethodDecl::isConstructor) - .map(md -> md.getParams().getParams().stream() - .map(p -> p.whenType(J.VariableDecls.class) - .map(mv -> mv.getVars().get(0).getSimpleName()) - .orElseThrow(() -> new RuntimeException("not possible to get here"))) - .allMatch(injectedFieldNames::contains)) - .orElse(false)); - } - - private List getInjectedFields(J.ClassDecl cd) { - return cd.getBody().getStatements().stream() - .filter(stat -> stat.whenType(J.VariableDecls.class).map(this::isFieldInjected).orElse(false)) - .map(J.VariableDecls.class::cast) - .collect(toList()); - } - - private boolean isFieldInjected(J.VariableDecls mv) { - return mv.getAnnotations().stream().anyMatch(this::isFieldInjectionAnnotation); - } - - private boolean isFieldInjectionAnnotation(J.Annotation ann) { - return isOfClassType(ann.getType(), "javax.inject.Inject") || - isOfClassType(ann.getType(), "org.springframework.beans.factory.annotation.Autowired"); - } -} diff --git a/tmp/ConstructorInjectionTest.kt b/tmp/ConstructorInjectionTest.kt deleted file mode 100644 index 7582753db..000000000 --- a/tmp/ConstructorInjectionTest.kt +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright 2020 the original author or authors. - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * https://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.openrewrite.java.spring - -import org.junit.jupiter.api.Test -import org.openrewrite.RefactorVisitor -import org.openrewrite.RefactorVisitorTestForParser -import org.openrewrite.java.JavaParser -import org.openrewrite.java.tree.J - -class ConstructorInjectionTest : RefactorVisitorTestForParser { - - override val parser: JavaParser = JavaParser.fromJavaVersion() - .classpath("spring-beans") - .build() - override val visitors: Iterable> = listOf() - - @Test - fun constructorInjection() = assertRefactored( - visitors = listOf(ConstructorInjection()), - before = """ - import org.springframework.beans.factory.annotation.Autowired; - - public class UsersController { - @Autowired - private UsersService usersService; - - @Autowired - UsernameService usernameService; - - public void setUsersService(UsersService usersService) { - this.usersService = usersService; - } - } - """, - after = """ - public class UsersController { - private final UsersService usersService; - - private final UsernameService usernameService; - - public UsersController(UsersService usersService, UsernameService usernameService) { - this.usersService = usersService; - this.usernameService = usernameService; - } - } - """ - ) - - @Test - fun constructorInjectionWithLombok() = assertRefactored( - visitors = listOf(ConstructorInjection().apply { setUseLombokRequiredArgsAnnotation(true) }), - before = """ - import org.springframework.beans.factory.annotation.Autowired; - - public class UsersController { - @Autowired - private UsersService usersService; - - @Autowired - UsernameService usernameService; - } - """, - after = """ - import lombok.RequiredArgsConstructor; - - @RequiredArgsConstructor - public class UsersController { - private final UsersService usersService; - - private final UsernameService usernameService; - } - """ - ) - - @Test - fun constructorInjectionWithJSR305() = assertRefactored( - visitors = listOf(ConstructorInjection().apply { setUseJsr305Annotations(true) }), - before = """ - import org.springframework.beans.factory.annotation.Autowired; - - public class UsersController { - @Autowired(required = false) - private UsersService usersService; - - @Autowired - UsernameService usernameService; - } - """, - after = """ - import javax.annotation.Nonnull; - - public class UsersController { - @Nonnull - private final UsersService usersService; - - private final UsernameService usernameService; - - public UsersController(UsersService usersService, UsernameService usernameService) { - this.usersService = usersService; - this.usernameService = usernameService; - } - } - """ - ) -} diff --git a/tmp/ValueToConfigurationProperties.java b/tmp/ValueToConfigurationProperties.java deleted file mode 100644 index b96cf6649..000000000 --- a/tmp/ValueToConfigurationProperties.java +++ /dev/null @@ -1,907 +0,0 @@ -/* - * Copyright 2020 the original author or authors. - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * https://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.openrewrite.java.spring.boot2; - -import org.openrewrite.AutoConfigure; -import org.openrewrite.Formatting; -import org.openrewrite.SourceFile; -import org.openrewrite.java.*; -import org.openrewrite.java.tree.J; -import org.openrewrite.java.tree.JavaType; -import org.openrewrite.java.tree.Statement; - -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static org.openrewrite.Formatting.EMPTY; -import static org.openrewrite.Tree.randomId; -import static org.openrewrite.internal.StringUtils.capitalize; -import static org.openrewrite.internal.StringUtils.uncapitalize; - -/** - * Notes on ValueToConfigurationProperties - *

- * 1. Scanning phase: Visit class fields, constructors for @Value annotations create a tree of @Value annotation contents. - * Given @Values containing these property paths: - * app.config.bar, app.config.foo, screen.resolution.horizontal, screen.resolution.vertical, screen.refresh-rate - * The resulting tree should be: - *

- *                root
- *            /         \
- *          app         screen
- *        /           /       \
- *    config     resolution    refreshRate
- *    /        /          \
- * bar     horizontal     vertical
- * 
- * Store list of classes where every field is @Value annotated as it can be reused instead of generating a new class - * Store a list of any existing @ConfigurationProperties classes - * Record list of fields whose names don't match the last piece of their @Value annotations. - * Leaf nodes of tree have links back to their original appearance(s) - *

- * 1.b.: Config Class Generation: - * For each subtree where there is not an existing ConfigurationProperties class, create a new (empty) ConfigurationProperties class - * Any new classes should be placed adjacent in the source tree to the Spring Application class - *

- * 2. Config Class Update Phase: - * Go through the config classes and create fields, getters, setters, corresponding to each node of the tree - *

- * 3. Reference Update phase: - * Go through ALL classes and anywhere anything @Value annotated appears, replace it with the corresponding @ConfigurationProperties type. - * May involve collapsing multiple arguments into a single argument, updating references to those arguments - */ -@AutoConfigure -public class ValueToConfigurationProperties extends JavaRefactorVisitor { - - private static final String valueAnnotationSignature = "@org.springframework.beans.factory.annotation.Value"; - private static final String configurationPropertiesFqn = "org.springframework.boot.context.properties.ConfigurationProperties"; - private static final String configurationPropertiesSignature = "@" + configurationPropertiesFqn; - private static final JavaType.Class configurationPropertiesAnnotationType = JavaType.Class.build( - configurationPropertiesFqn, - Collections.emptyList(), - Collections.emptyList(), - Collections.singletonList(JavaType.Class.build("java.lang.annotation.Annotation")), - Collections.emptyList(), - null); - private static final String springBootApplicationSignature = "@org.springframework.boot.autoconfigure.SpringBootApplication"; - - // Visible for testing - PrefixParentNode prefixTree = PrefixTree.build(); - J.CompilationUnit springBootApplication; - private boolean configClassesGenerated = false; - JavaParser jp; - /** - * Keep track of any classes generated by name. - * Will just be the stubs, before any fields/getters/setters have been filled in - */ - private Map generatedClasses; - - public ValueToConfigurationProperties() { - setCursoringOn(); - } - - @Override - public J visitCompilationUnit(J.CompilationUnit cu) { - if (configClassesGenerated && springBootApplication != null) { - andThen(new PopulateConfigurationPropertiesClasses()); - andThen(new UpdateReferences()); - andThen(new RemoveValueAnnotations()); - } - return super.visitCompilationUnit(cu); - } - - @Override - public J visitClassDecl(J.ClassDecl classDecl) { - if (!configClassesGenerated) { - classDecl.getFields() - .forEach(prefixTree::put); - classDecl.getMethods() - .forEach(prefixTree::put); - // Any generated config classes will adopt the package of the Spring Boot Application class - // and be placed adjacent to it in the source tree - if (classDecl.findAnnotations(springBootApplicationSignature).size() > 0) { - J.CompilationUnit cu = getCursor().firstEnclosing(J.CompilationUnit.class); - assert cu != null; - springBootApplication = cu; - jp = cu.buildParser(); - } - } - return super.visitClassDecl(classDecl); - } - - - @Override - public Collection generate() { - if(!configClassesGenerated && springBootApplication != null) { - generatedClasses = new HashMap<>(); - configClassesGenerated = true; - return prefixTree.getLongestCommonPrefixes().stream().map( commonPrefix -> { - - String className = prefixTree.get(commonPrefix).getEnclosingClassName(); - String peckage = (springBootApplication.getPackageDecl() == null) ? "" : springBootApplication.getPackageDecl().printTrimmed() + ";\n\n"; - String newClassText = peckage + - "import org.springframework.boot.context.properties.ConfigurationProperties;\n\n" + - "@ConfigurationProperties(\"" + String.join(".", commonPrefix) + "\")\n" + - "public class " + className + " {\n" + - "}\n"; - Path parentDir = springBootApplication.getSourcePath().getParent(); - Path sourcePath; - if(parentDir == null) { - sourcePath = Paths.get(className + ".java"); - } else { - sourcePath = parentDir.resolve(className + ".java"); - } - J.CompilationUnit cu = fillConfigurationPropertiesTypeAttribution( - jp.reset().parse(newClassText) - .get(0) - .withSourcePath(sourcePath)); - generatedClasses.put(className, cu); - return cu; - - }).collect(Collectors.toList()); - } else { - return Collections.emptyList(); - } - } - private static J.CompilationUnit fillConfigurationPropertiesTypeAttribution(J.CompilationUnit cu) { - return (J.CompilationUnit) new FillTypeAttributions(new JavaType.Class[]{configurationPropertiesAnnotationType}).visitCompilationUnit(cu); - } - - private class PopulateConfigurationPropertiesClasses extends JavaRefactorVisitor { - @Override - public J visitClassDecl(J.ClassDecl classDecl) { - J.ClassDecl cd = refactor(classDecl, super::visitClassDecl); - List configPropsAnnotations = cd.findAnnotations(configurationPropertiesSignature); - if (configPropsAnnotations.size() > 0) { - J.Annotation configPropsAnnotation = configPropsAnnotations.get(0); - if (configPropsAnnotation.getArgs() == null) { - return cd; - } - String configPropsPrefix = (String) ((J.Literal) configPropsAnnotation.getArgs().getArgs().get(0)).getValue(); - if (configPropsPrefix == null) { - return cd; - } - PrefixParentNode treeForConfigPropsClass = (PrefixParentNode)prefixTree.get(configPropsPrefix); - for(PrefixTree pt : treeForConfigPropsClass.children.values()) { - generateClassesFieldsGettersSetters(cd, pt); - } - } - return cd; - } - - /** - * Based on the contents of the supplied PrefixTree, create inner classes, fields, getters, and setters - * Given a class declaration like: - * class Example {} - * and a PrefixTree like this, assuming all properties are Strings - * root - * | - * app - * | - * config - * / \ - * foo bar - * - * Example will end up looking similar to the following (whitespace and ordering of fields/methods not guaranteed): - * - * class Example { - * private AppConfig appConfig; - * public AppConfig getAppConfig() { return appConfig; } - * public void setAppConfig(AppConfig value) { appConfig = value; } - * public static class AppConfig { - * private String foo; - * public String getFoo() { return foo; } - * public void setFoo(String value) { foo = value; } - * private String bar; - * public String getBar() { return bar; } - * public void setBar(String value) { bar = value; } - * } - * } - */ - private void generateClassesFieldsGettersSetters(J.ClassDecl cd, PrefixTree pt) { - if(pt instanceof PrefixTerminalNode) { - PrefixTerminalNode node = (PrefixTerminalNode) pt; - String fieldName = node.name; - String fieldTypeExpr = node.type.toTypeTree().printTrimmed(); - andThen(new AddField.Scoped(cd, - Collections.singletonList(new J.Modifier.Private(randomId(), EMPTY)), - fieldTypeExpr, - fieldName, - null)); - andThen(new GenerateGetter.Scoped(cd, fieldName)); - andThen(new GenerateSetter.Scoped(cd, fieldName)); - } else if(pt instanceof PrefixParentNode) { - // Search through any inner class declarations that may exist, use existing declaration if one exists - String fieldName = pt.getName(); - String innerClassName = capitalize(fieldName); - Optional maybeInnerClassDecl = cd.getBody().getStatements().stream() - .filter(it -> it instanceof J.ClassDecl) - .map(J.ClassDecl.class::cast) - .filter(it -> it.getSimpleName().equals(innerClassName)) - .findAny(); - - J.ClassDecl innerClassDecl; - if(maybeInnerClassDecl.isPresent()) { - innerClassDecl = maybeInnerClassDecl.get(); - } else { - innerClassDecl = treeBuilder.buildInnerClassDeclaration( - cd, - "public static class " + innerClassName + " {\n}\n"); - List withNewDecl = cd.getBody().getStatements(); - withNewDecl.add(innerClassDecl); - cd = cd.withBody(cd.getBody().withStatements(withNewDecl)); - andThen(new AutoFormat(innerClassDecl)); - } - assert innerClassDecl.getType() != null; - andThen(new AddField.Scoped(cd, - Collections.singletonList(new J.Modifier.Private(randomId(), EMPTY)), - innerClassDecl.getType().getFullyQualifiedName(), - fieldName, - null)); - andThen(new GenerateGetter.Scoped(cd, fieldName)); - andThen(new GenerateSetter.Scoped(cd, fieldName)); - // There needs to be a field and getter/setter for the inner class. Any of these may already exist - - // Recurse on any children - ((PrefixParentNode) pt).children.values().forEach(subTree -> generateClassesFieldsGettersSetters(innerClassDecl, subTree)); - } - } - } - - /** - * Where fields or constructor parameters previously had @Value annotations, this will update them - * to be initialized via ConfigurationProperties classes passed into their constructor(s) - */ - private class UpdateReferences extends JavaRefactorVisitor { - @Override - public J visitClassDecl(J.ClassDecl classDecl) { - J.ClassDecl cd = refactor(classDecl, super::visitClassDecl); - List valueAnnotatedFields = cd.getFields().stream() - .filter(it -> it.findAnnotations(valueAnnotationSignature).size() > 0) - .collect(Collectors.toList()); - List valueAnnotatedConstructors = cd.getMethods().stream() - .filter(it -> it.getReturnTypeExpr() == null) - .filter(it -> it.findAnnotations(valueAnnotationSignature).size() > 0) - .collect(Collectors.toList()); - if(valueAnnotatedFields.size() > 0 || valueAnnotatedConstructors.size() > 0) { - andThen(new Scoped(cd, valueAnnotatedFields, valueAnnotatedConstructors)); - } - return cd; - } - private class Scoped extends JavaRefactorVisitor { - final J.ClassDecl scope; - final List valueAnnotatedFields; - final List valueAnnotatedConstructors; - - private Scoped(J.ClassDecl scope, List valueAnnotatedFields, List valueAnnotatedConstructors) { - this.scope = scope; - this.valueAnnotatedFields = valueAnnotatedFields; - this.valueAnnotatedConstructors = valueAnnotatedConstructors; - setCursoringOn(); - } - - @Override - public J visitCompilationUnit(J.CompilationUnit cu) { - - return super.visitCompilationUnit(cu); - } - - @Override - public J visitClassDecl(J.ClassDecl classDecl) { - J.ClassDecl cd = refactor(classDecl, super::visitClassDecl); - if(!scope.isScope(cd)) { - return cd; - } - List constructors = cd.getMethods().stream() - .filter(it -> it.getReturnTypeExpr() == null) - .collect(Collectors.toList()); - - // We'll put all the constructors back in, later - List bodyStatementsWithoutConstructors = cd.getBody().getStatements(); - bodyStatementsWithoutConstructors.removeAll(constructors); - - // If there's no explicit constructor, add one - if(constructors.size() == 0) { - J.MethodDecl newConstructor = treeBuilder.buildMethodDeclaration(cd, - "\npublic " + cd.getSimpleName() + "() {\n}\n\n"); - andThen(new AutoFormat(newConstructor)); - List withNewConstructor = cd.getBody().getStatements(); - withNewConstructor.add(0, newConstructor); - cd = cd.withBody(cd.getBody().withStatements(withNewConstructor)); - - constructors.add(newConstructor); - } - // Get list of each @ConfigurationProperties class that needs to be passed in to the constructors for this class - List prefixes = Stream.concat( - valueAnnotatedFields.stream() - .flatMap(field -> field.findAnnotations(valueAnnotationSignature).stream()), - valueAnnotatedConstructors.stream() - .flatMap(constructor -> constructor.getParams().getParams().stream() - .filter(decl -> decl instanceof J.VariableDecls) - .map(J.VariableDecls.class::cast) - .flatMap(decl -> decl.findAnnotations(valueAnnotationSignature).stream())) - ) - .filter(Objects::nonNull) - .map(ValueToConfigurationProperties::getValueValue) - .distinct() - .map(it -> prefixTree.get(it)) - .collect(Collectors.toList()); - - List configPropsCompilationUnits = prefixes.stream() - .map(PrefixTree::getEnclosingClassName) - .distinct() - .map(generatedClasses::get) - .collect(Collectors.toList()); - - // Add imports for the config classes - configPropsCompilationUnits.stream() - .map(cu -> cu.getClasses().get(0).getType()) - .filter(Objects::nonNull) - .map(JavaType.FullyQualified::getFullyQualifiedName) - .forEach(this::maybeAddImport); - - // There might be a more generic "add parameter to method declaration" visitor to be extracted from this - final J.ClassDecl finalCd = cd; - constructors = constructors.stream() - .map(constructor -> { - // Add parameters for each ConfigurationProperties class to each constructor - for (J.CompilationUnit configPropsCu : configPropsCompilationUnits) { - J.MethodDecl.Parameters parameters = constructor.getParams(); - List parameterStatements = parameters.getParams(); - J.ClassDecl configPropsClass = configPropsCu.getClasses().get(0); - J.VariableDecls newParam = treeBuilder.buildFieldDeclaration(finalCd, - configPropsClass.getSimpleName() + " " + uncapitalize(configPropsClass.getSimpleName() + ";"), - configPropsClass.getType() - ) - .withFormatting(Formatting.format(" ")); - parameterStatements.add(newParam); - parameters = parameters.withParams(parameterStatements); - if(constructor.getModifiers().size() > 0) { - // Make sure there's a space between the access modifier and the name of the constructor - constructor = constructor.withName(constructor.getName().withFormatting(Formatting.format(" "))); - } - constructor = constructor.withParams(parameters); - } - return constructor; - }) - .map(constructor -> { - /* - * Move any @Value annotated constructor parameters to be variables internal to the constructor - * So something like this: - * public A(@Value("${foo.bar}") String param) { - * // does something with param - * } - * Becomes: - * public A() { - * String param = fooConfiguration.getBar(); - * // still able to do stuff with param - * } - * - * Subsequent steps within this chain of map() calls will add the "fooConfiguration" parameter - */ - J.MethodDecl.Parameters parameters = constructor.getParams(); - List valueAnnotatedParams = parameters.getParams().stream() - .filter(param -> param instanceof J.VariableDecls) - .map(J.VariableDecls.class::cast) - .filter(param -> param.findAnnotations(valueAnnotationSignature).size() > 0) - .collect(Collectors.toList()); - - List assignmentStatementsToAdd = valueAnnotatedParams.stream() - .map(param -> { - PrefixTerminalNode node = prefixTree.get(param); - String initializingExpression = node.getInitializingExpression(); - assert param.getTypeExpr() != null; - return (J.VariableDecls) treeBuilder.buildSnippet(getCursor(), - param.getTypeExpr().print() + " " + param.getVars().get(0).getSimpleName() + " = " + initializingExpression + ";", - param.getTypeAsClass()).get(0); - }) - .collect(Collectors.toList()); - J.Block body = getBodyOrEmptyBlock(constructor); - List statements = body.getStatements(); - for(J.VariableDecls statementToAdd : assignmentStatementsToAdd) { - // Inserting at the beginning so the assignments will come before any later statement which may reference them - // But what if the constructor body begins with a this() or super() call? - // That case isn't handled right now - statements.add(0, statementToAdd); - } - constructor = constructor.withBody(body.withStatements(statements)); - - // Remove any parameters which were annotated with @Value - List withoutValueParams = parameters.getParams(); - withoutValueParams.removeAll(valueAnnotatedParams); - return constructor.withParams(parameters.withParams(withoutValueParams)); - }) - .map(constructor -> { - // Add statements to the body of the constructor initializing all @Value annotated fields - J.Block body = getBodyOrEmptyBlock(constructor); - List statements = body.getStatements(); - for(J.VariableDecls field : valueAnnotatedFields) { - PrefixTerminalNode node = prefixTree.get(field); - String initializingExpression = node.getInitializingExpression(); - J.Assign assignment = (J.Assign) treeBuilder.buildSnippet(getCursor(), - "this." + field.getVars().get(0).getSimpleName() + " = " + initializingExpression + ";", - field.getTypeExpr().getType()).get(0); - - // Area to improve: - // None of the statements/expressions in the above snippet are going to come out - // type-attributed correctly. Which could cause confusion/trouble for visitors coming later - statements.add(assignment); - } - // Make sure the "}" doesn't end up on the same line as the final statement - body = body.withStatements(statements) - .withEnd(body.getEnd().withPrefix("\n")); - return constructor.withBody(body); - }) - .collect(Collectors.toList()); - // Put the modified constructors back into the class body and clean up their formatting - constructors.forEach(it -> andThen(new AutoFormat(it))); - bodyStatementsWithoutConstructors.addAll(0, constructors); - cd = cd.withBody(cd.getBody().withStatements(bodyStatementsWithoutConstructors)); - - return cd; - } - } - } - - /** - * Finally removes @Value annotations wherever they may be found. - */ - private class RemoveValueAnnotations extends JavaRefactorVisitor { - @Override - public J visitMultiVariable(J.VariableDecls multiVariable) { - J.VariableDecls mv = refactor(multiVariable, super::visitMultiVariable); - List valueAnnotations = mv.findAnnotations(valueAnnotationSignature); - if(valueAnnotations.size() > 0) { - List otherAnnotations = mv.getAnnotations(); - otherAnnotations.removeAll(valueAnnotations); - mv = mv.withAnnotations(otherAnnotations); - } - return mv; - } - - @Override - public J visitMethod(J.MethodDecl method) { - J.MethodDecl m = refactor(method, super::visitMethod); - // Only applicable to constructors which are the only method declarations with no return type - if(m.getReturnTypeExpr() == null) { - List argsWithoutValueAnnotations = m.getParams().getParams().stream() - .map(it -> { - if (it instanceof J.VariableDecls) { - J.VariableDecls param = (J.VariableDecls) it; - List valueAnnotations = param.findAnnotations(valueAnnotationSignature); - List otherAnnotations = param.getAnnotations(); - otherAnnotations.removeAll(valueAnnotations); - return param.withAnnotations(otherAnnotations); - } else { - return it; - } - }) - .collect(Collectors.toList()); - m = m.withParams(m.getParams().withParams(argsWithoutValueAnnotations)); - } - return m; - } - } - /** - * Get the existing body of the method or an empty block if the method has no body - */ - private static J.Block getBodyOrEmptyBlock(J.MethodDecl methodDecl) { - J.Block result = methodDecl.getBody(); - if(result == null) { - result = new J.Block<>(randomId(), null, new ArrayList<>(), EMPTY, new J.Block.End(randomId(), EMPTY)); - } - return result; - } - - /** - * Extracts, de-dashes, and camelCases the value string from a @Value annotation - * Given: @Value("${app.screen.refresh-rate}") - * Returns: app.screen.refreshRate - */ - private static String getValueValue(J.Annotation value) { - assert value.getArgs() != null; - String valueValue = (String) ((J.Literal) value.getArgs().getArgs().get(0)).getValue(); - assert valueValue != null; - valueValue = valueValue.replace("${", "") - .replace("}", ""); - valueValue = Arrays.stream(valueValue.split("-")) - .map(part -> Character.toUpperCase(part.charAt(0)) + part.substring(1)) - .collect(Collectors.joining("")); - return Character.toLowerCase(valueValue.charAt(0)) + valueValue.substring(1); - } - - interface PrefixTree { - String getName(); - - PrefixTree put(List pathSegments, JavaType type); - - default PrefixTree get(String path) { - return get(Arrays.asList(path.split("\\."))); - } - - PrefixTree get(List pathSegments); - - PrefixParentNode getParent(); - - default boolean isRoot() { return getParent() == null; } - - /** - * Get the name of the outermost class that will be generated to contain properties with the current prefix. - * Example output: - *

-         *     PrefixTree     getClassName()
-         *     root           ""
-         *      |
-         *     app            "App"
-         *      |
-         *     config         "AppConfigConfiguration"
-         *      |   \
-         *      |   prop      "AppConfigConfiguration"
-         *     intermediate   "AppConfigConfiguration"
-         *      |
-         *     terminal       "AppConfigConfiguration"
-         * 
- */ - String getEnclosingClassName(); - - /** - * Get a string representation of an expression that would produce the appropriate value for this node - *
-         *     PrefixTree     getInitializingExpression()
-         *     root           ""
-         *      |
-         *     app            "App"
-         *      |
-         *     config         "appConfigConfiguration"
-         *      |   \
-         *      |   prop      "appConfigConfiguration.getProp()"
-         *     intermediate   "appConfigConfiguration.getIntermediate()"
-         *      |
-         *     terminal       "appConfigConfiguration.getIntermediate().getTerminal()"
-         * 
- */ - String getInitializingExpression(); - - /** - * Return true if the current node is part of the common prefix for its tree - * A node is part of the common prefix iff: - * 1. It is the root node OR - * 2. Its parent is part of the common prefix AND it is the only child of its parent node - * - *
-         *     PrefixTree     isPartOfCommonPrefix()
-         *     root           true
-         *      |
-         *     app            true
-         *      |
-         *     config         true
-         *      |   \
-         *      |   prop      false
-         *     intermediate   false
-         *      |
-         *     terminal       false
-         * 
- */ - boolean isPartOfCommonPrefix(); - - default String getFullPath() { - if(isRoot()) { - return ""; - } - if(getParent().isRoot()) { - return getName(); - } else { - return getParent().getFullPath() + "." + getName(); - } - } - - static PrefixParentNode build() { - return new PrefixParentNode(null, "root"); - } - } - - /** - * A node of a PrefixTree with no children of its own. - * Keeps track of the element or elements which reference it - */ - static class PrefixTerminalNode implements PrefixTree { - final String name; - final PrefixParentNode parent; - final JavaType type; - - public PrefixTerminalNode(PrefixParentNode parent, String name, JavaType type) { - this.name = name; - this.parent = parent; - this.type = type; - } - - @Override - public PrefixTree put(List pathSegments, JavaType type) { - if (pathSegments == null) { - throw new IllegalArgumentException("pathSegments may not be null"); - } - if (type != null && this.type != type) { - throw new IllegalArgumentException( - "terminal node cannot have two types. type is currently recorded as \"" + - this.type.toTypeTree().printTrimmed() + "\", cannot reassign it to \"" + - type.toTypeTree().printTrimmed()); - } - if (pathSegments.size() > 1) { - throw new IllegalArgumentException("Cannot add new path segment to terminal node"); - } - return this; - } - - @Override - public PrefixTree get(List pathSegments) { - if (pathSegments == null) { - throw new IllegalArgumentException("pathSegments may not be null"); - } - if (pathSegments.size() == 0) { - return this; - } else if (pathSegments.size() == 1 && pathSegments.get(0).equals(name)) { - return this; - } else { - throw new IllegalArgumentException( - "Terminal node \"" + name + "\" does not match requested path \"" + - String.join(".", pathSegments) + "\""); - } - } - - @Override - public PrefixParentNode getParent() { - return parent; - } - - @Override - public String getEnclosingClassName() { - return parent.getEnclosingClassName(); - } - - @Override - public String getInitializingExpression() { - return parent.getInitializingExpression() + ".get" + capitalize(name) + "()"; - } - - @Override - public boolean isPartOfCommonPrefix() { - return false; - } - - @Override - public String getName() { - return name; - } - } - - /** - * A PrefixTree with children. Has no data except via its child nodes. Get an instance via PrefixTree.build() - */ - static class PrefixParentNode implements PrefixTree { - final String name; - final Map children = new HashMap<>(); - final PrefixParentNode parent; - - private PrefixParentNode(PrefixParentNode parent, String name) { - this.name = name; - this.parent = parent; - } - - PrefixTree buildChild(List pathSegments, JavaType type) { - if (pathSegments.size() == 0) { - throw new IllegalArgumentException("pathSegments may not be null"); - } - String nodeName = pathSegments.get(0); - List remainingSegments = pathSegments.subList(1, pathSegments.size()); - if (remainingSegments.size() == 0) { - return new PrefixTerminalNode(this, nodeName, type); - } else { - PrefixParentNode intermediateNode = new PrefixParentNode(this, nodeName); - PrefixTree child = intermediateNode.buildChild(remainingSegments, type); - intermediateNode.children.put(child.getName(), child); - return intermediateNode; - } - } - - void put(J.VariableDecls field) { - List valueAnnotations = field.findAnnotations(valueAnnotationSignature); - if (valueAnnotations.size() == 0) { - return; - } - J.Annotation valueAnnotation = valueAnnotations.get(0); - String path = getValueValue(valueAnnotation); - List pathSegments = Arrays.asList(path.split("\\.")); - if(field.getTypeExpr() != null) { - put(pathSegments, field.getTypeExpr().getType()); - } - } - - /** - * Get the terminal node matching the @Value annotation on the field parameter, or null if no such - * terminal node exists or the field parameter is not @Value-annotated. - */ - PrefixTerminalNode get(J.VariableDecls field) { - List valueAnnotations = field.findAnnotations(valueAnnotationSignature); - if (valueAnnotations.size() == 0) { - return null; - } - J.Annotation valueAnnotation = valueAnnotations.get(0); - String path = getValueValue(valueAnnotation); - return (PrefixTerminalNode) get(Arrays.asList(path.split("\\."))); - } - - void put(J.MethodDecl methodDecl) { - methodDecl.getParams().getParams().stream() - .filter(param -> param instanceof J.VariableDecls) - .map(J.VariableDecls.class::cast) - .filter(param -> param.findAnnotations(valueAnnotationSignature).size() > 0) - .forEach(param -> { - if(param.getTypeExpr() != null) { - J.Annotation valueAnnotation = param.findAnnotations(valueAnnotationSignature).get(0); - List pathSegments = Arrays.asList(getValueValue(valueAnnotation).split("\\.")); - put(pathSegments, param.getTypeExpr().getType()); - } - }); - } - - @Override - public PrefixTree put(List pathSegments, JavaType type) { - if (pathSegments == null) { - throw new IllegalArgumentException("pathSegments may not be null"); - } - if (pathSegments.size() == 0) { - throw new IllegalArgumentException("pathSegments may not be empty"); - } - String nodeName = pathSegments.get(0); - - if (children.containsKey(nodeName)) { - PrefixTree existingNode = children.get(nodeName); - existingNode.put(pathSegments.subList(1, pathSegments.size()), type); - } else { - children.put(nodeName, buildChild(pathSegments, type)); - } - return this; - } - - @Override - public PrefixTree get(List pathSegments) { - if (pathSegments == null) { - throw new IllegalArgumentException("pathSegments may not be null"); - } - if (pathSegments.size() == 0) { - return this; - } - String nodeName = pathSegments.get(0); - List remainingSegments = pathSegments.subList(1, pathSegments.size()); - if (children.containsKey(nodeName)) { - return children.get(nodeName).get(remainingSegments); - } else { - return null; - } - } - - @Override - public PrefixParentNode getParent() { - return parent; - } - - @Override - public String getEnclosingClassName() { - if(isRoot()) { - return ""; - } - if(isPartOfCommonPrefix()) { - String extra = ""; - if(children.size() > 1 || !children.values().stream().allMatch(it -> it instanceof PrefixParentNode && it.isPartOfCommonPrefix())) { - extra = "Configuration"; - } - return getParent().getEnclosingClassName() + capitalize(getName()) + extra; - } else { - return getParent().getEnclosingClassName(); - } - } - - @Override - public String getInitializingExpression() { - if(isRoot()) { - return ""; - } - if(isPartOfCommonPrefix()) { - String extra = ""; - String resultName = parent.getInitializingExpression() + capitalize(name); - if(children.size() > 1 || !children.values().stream().allMatch(it -> it instanceof PrefixParentNode && it.isPartOfCommonPrefix())) { - resultName = uncapitalize(resultName); - extra = "Configuration"; - } - return resultName + extra; - } else { - return getParent().getInitializingExpression() + ".get" + capitalize(name) + "()"; - } - } - - @Override - public boolean isPartOfCommonPrefix() { - if(isRoot() || parent.isRoot()) { - return true; - } - return parent.isPartOfCommonPrefix() - && parent.children.size() == 1 - && parent.children.values().iterator().next() == this; - } - - public boolean allChildrenTerminal() { - return children.values().stream().allMatch(it -> it instanceof PrefixTerminalNode); - } - - public boolean onlyChildIsParentNode() { - return (children.size() == 1 && children.values().iterator().next() instanceof PrefixParentNode); - } - - @Override - public String getName() { - return name; - } - - /** - * Return the longest paths down each branch of the tree for which there is exactly one non-terminal child - * or no non-terminal children and any number of terminal children. - * - * So for a tree like: - * root - * / \ - * app screen - * / / \ - * config refreshRate resolution - * / \ - * foo bar - * - * This will return a list like - * [app.config, screen] - */ - public List> getLongestCommonPrefixes() { - List> result = new ArrayList<>(); - for (PrefixTree subtree : children.values()) { - if (subtree instanceof PrefixParentNode) { - List intermediate = new ArrayList<>(); - getUntilTerminalOrMultipleChildren(intermediate, (PrefixParentNode) subtree); - result.add(intermediate); - } else { - List root = Collections.singletonList("root"); - if (!result.contains(root)) { - result.add(root); - } - } - } - return result; - } - - private void getUntilTerminalOrMultipleChildren(List resultSoFar, PrefixParentNode parentNode) { - PrefixTree node = parentNode; - List children; - do { - if (!(node instanceof PrefixParentNode)) { - break; - } - resultSoFar.add(node.getName()); - children = new ArrayList<>(((PrefixParentNode) node).children.values()); - node = children.get(0); - } while (children.size() == 1 && children.get(0) instanceof PrefixParentNode); - } - } -} diff --git a/tmp/ValueToConfigurationPropertiesTest.kt b/tmp/ValueToConfigurationPropertiesTest.kt deleted file mode 100644 index 44e8c8b2e..000000000 --- a/tmp/ValueToConfigurationPropertiesTest.kt +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright 2020 the original author or authors. - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * https://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.openrewrite.java.spring.boot2 - -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test -import org.openrewrite.* -import org.openrewrite.java.JavaParser -import org.openrewrite.java.tree.J -import org.openrewrite.java.tree.JavaType - -class ValueToConfigurationPropertiesTest : RefactorVisitorTestForParser { - override val parser: JavaParser = JavaParser.fromJavaVersion() - .classpath("spring-boot", "spring-beans") - .build() - override val visitors: Iterable> = listOf(ValueToConfigurationProperties()) - - @Test - fun testBasicVTCP() { - val springApplication = """ - package org.example; - import org.springframework.boot.autoconfigure.SpringBootApplication; - @SpringBootApplication - public class ASpringBootApplication { - public static void main(String[] args) {} - } - """.trimIndent() - val classUsingValue = """ - package org.example; - import org.springframework.beans.factory.annotation.Value; - - public class ExampleValueClass { - public ExampleValueClass(@Value("${"$"}{app.config.constructor-param}") String baz) {} - @Value("${"$"}{app.config.foo}") - String foo; - - @Value("${"$"}{app.config.bar}") - String bar; - - @Value("${"$"}{screen.resolution.height}") - int height; - - @Value("${"$"}{screen.resolution.width}") - int width; - - @Value("${"$"}{screen.refresh-rate}") - int refreshRate; - } - """.trimIndent() - val vtcp = ValueToConfigurationProperties() - val results = Refactor() - .visit(vtcp) - .fix(parser.parse(classUsingValue, springApplication)) - .map { it.fixed as J.CompilationUnit } - - val prefixtree = vtcp.prefixTree - - val foo = prefixtree.get("app.config.foo") - assertThat(foo).isNotNull - assertThat(foo).isInstanceOf(ValueToConfigurationProperties.PrefixTerminalNode::class.java) - val bar = prefixtree.get("app.config.bar") - assertThat(bar).isNotNull - assertThat(bar).isInstanceOf(ValueToConfigurationProperties.PrefixTerminalNode::class.java) - val refreshRate = prefixtree.get("screen.refreshRate") - assertThat(refreshRate).isNotNull - assertThat(refreshRate).isInstanceOf(ValueToConfigurationProperties.PrefixTerminalNode::class.java) - val width = prefixtree.get("screen.resolution.height") - assertThat(width).isNotNull - assertThat(width).isInstanceOf(ValueToConfigurationProperties.PrefixTerminalNode::class.java) - - val config = prefixtree.get("app.config") - assertThat(config).isNotNull - assertThat(config).isInstanceOf(ValueToConfigurationProperties.PrefixParentNode::class.java) - - val longestCommonPrefixPaths = prefixtree.longestCommonPrefixes - assertThat(longestCommonPrefixPaths).isNotEmpty - assertThat(longestCommonPrefixPaths).contains(listOf("app", "config"), listOf("screen")) - - // Now check out some actual results - assertThat(results.size).isEqualTo(3) - .`as`("There should be the generated AppConfigConfiguration and ScreenConfiguration classes, " + - "plus a modified ExampleValueClass") - - val appConfig = results.find { it.classes.first().name.simpleName == "AppConfigConfiguration" } - assertThat(appConfig).isNotNull - val screenConfig = results.find { it.classes.first().name.simpleName == "ScreenConfiguration" } - assertThat(screenConfig).isNotNull - val exampleValue = results.find { it.classes.first().name.simpleName == "ExampleValueClass" } - assertThat(exampleValue).isNotNull - - appConfig.assertHasMethod("getFoo", "setFoo", "getConstructorParam", "setConstructorParam") - screenConfig.assertHasMethod("getResolution", "setResolution", "getRefreshRate", "setRefreshRate") - // The constructor of ExampleValue should have been amended to accept an AppConfigConfiguration - // and a ScreenConfiguration as its two arguments - val constructors = exampleValue!!.classes.first().methods.filter { it.isConstructor } - assertThat(constructors.size).isEqualTo(1) - val constructor = constructors.first() - val constructorParams = constructor.params.params - assertThat(constructorParams.size).isEqualTo(2) - assertThat(constructorParams.find { it.hasClassType(JavaType.Class.build("org.example.AppConfigConfiguration")) }).isNotNull - assertThat(constructorParams.find { it.hasClassType(JavaType.Class.build("org.example.ScreenConfiguration")) }).isNotNull - } - - private fun J.CompilationUnit?.assertHasMethod(vararg names: String) = - names.forEach { name -> - assertThat(this!!.classes.first().methods.find { it.name.simpleName == name }).isNotNull - } - - -}