diff --git a/src/main/java/tech/jhipster/lite/module/infrastructure/secondary/javadependency/gradle/GradleCommandHandler.java b/src/main/java/tech/jhipster/lite/module/infrastructure/secondary/javadependency/gradle/GradleCommandHandler.java index 65acff8b53b..77ae0df5384 100644 --- a/src/main/java/tech/jhipster/lite/module/infrastructure/secondary/javadependency/gradle/GradleCommandHandler.java +++ b/src/main/java/tech/jhipster/lite/module/infrastructure/secondary/javadependency/gradle/GradleCommandHandler.java @@ -1,15 +1,12 @@ package tech.jhipster.lite.module.infrastructure.secondary.javadependency.gradle; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Collection; -import java.util.List; +import static tech.jhipster.lite.module.domain.replacement.ReplacementCondition.*; + import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.commons.lang3.NotImplementedException; -import com.electronwill.nightconfig.core.Config; -import com.electronwill.nightconfig.core.file.FileConfig; -import com.electronwill.nightconfig.core.io.ParsingException; import tech.jhipster.lite.common.domain.ExcludeFromGeneratedCodeCoverage; import tech.jhipster.lite.error.domain.Assert; @@ -24,12 +21,12 @@ import tech.jhipster.lite.module.domain.javabuild.command.RemoveJavaDependencyManagement; import tech.jhipster.lite.module.domain.javabuild.command.SetVersion; import tech.jhipster.lite.module.domain.javadependency.JavaDependency; -import tech.jhipster.lite.module.domain.javadependency.JavaDependencyVersion; import tech.jhipster.lite.module.domain.properties.JHipsterProjectFolder; import tech.jhipster.lite.module.domain.replacement.ContentReplacers; import tech.jhipster.lite.module.domain.replacement.MandatoryFileReplacer; import tech.jhipster.lite.module.domain.replacement.MandatoryReplacer; import tech.jhipster.lite.module.domain.replacement.RegexNeedleBeforeReplacer; +import tech.jhipster.lite.module.domain.replacement.RegexReplacer; import tech.jhipster.lite.module.infrastructure.secondary.FileSystemReplacer; import tech.jhipster.lite.module.infrastructure.secondary.javadependency.JavaDependenciesCommandHandler; @@ -37,8 +34,6 @@ public class GradleCommandHandler implements JavaDependenciesCommandHandler { private static final String COMMAND = "command"; private static final String NOT_YET_IMPLEMENTED = "Not yet implemented"; - private static final String VERSIONS_TOML_KEY = "versions"; - private static final String LIBRARIES_TOML_KEY = "libraries"; private static final String BUILD_GRADLE_FILE = "build.gradle.kts"; private static final Pattern GRADLE_DEPENDENCY_NEEDLE = Pattern.compile("^\\s+// jhipster-needle-gradle-add-dependency$", Pattern.MULTILINE); @@ -46,7 +41,7 @@ public class GradleCommandHandler implements JavaDependenciesCommandHandler { private final Indentation indentation; private final JHipsterProjectFolder projectFolder; - private final FileConfig versionsCatalog; + private final VersionsCatalog versionsCatalog; private final FileSystemReplacer fileReplacer = new FileSystemReplacer(); public GradleCommandHandler(Indentation indentation, JHipsterProjectFolder projectFolder) { @@ -55,81 +50,62 @@ public GradleCommandHandler(Indentation indentation, JHipsterProjectFolder proje this.indentation = indentation; this.projectFolder = projectFolder; - Path tomlVersionCatalogFile = tomlVersionCatalogPath(); - versionsCatalog = FileConfig.builder(tomlVersionCatalogFile) - .sync() - .build(); - - // Missing TOML file will be automatically created, but its parent folder should exist - if (!Files.exists(tomlVersionCatalogFile.getParent())) { - tomlVersionCatalogFile.toFile().getParentFile().mkdirs(); - } - - try { - versionsCatalog.load(); - } catch (ParsingException exception) { - throw new InvalidTomlVersionCatalogException(exception); - } - } - - private Path tomlVersionCatalogPath() { - return projectFolder.filePath("gradle").resolve("libs.versions.toml"); + this.versionsCatalog = new VersionsCatalog(projectFolder); } @Override public void handle(SetVersion command) { Assert.notNull(COMMAND, command); - JavaDependencyVersion javaDependencyVersion = command.version(); - versionsCatalog.set(VERSIONS_TOML_KEY + "." + javaDependencyVersion.slug().slug(), javaDependencyVersion.version().get()); - - versionsCatalog.save(); + versionsCatalog.setVersion(command.version()); } @Override public void handle(AddDirectJavaDependency command) { Assert.notNull(COMMAND, command); - addJavaDependencyToVersionCatalog(command.dependency()); + versionsCatalog.addLibrary(command.dependency()); addJavaDependencyToBuildGradle(command.dependency()); } private void addJavaDependencyToBuildGradle(JavaDependency dependency) { - GradleDependencyScope gradleScope = switch(dependency.scope()) { - case TEST -> GradleDependencyScope.TEST_IMPLEMENTATION; - case PROVIDED -> GradleDependencyScope.COMPILE_ONLY; - case RUNTIME -> GradleDependencyScope.RUNTIME_ONLY; - default -> GradleDependencyScope.IMPLEMENTATION; - }; + GradleDependencyScope gradleScope = gradleDependencyScope(dependency); MandatoryReplacer replacer = new MandatoryReplacer( new RegexNeedleBeforeReplacer( (contentBeforeReplacement, newText) -> !contentBeforeReplacement.contains(newText), gradleScope == GradleDependencyScope.TEST_IMPLEMENTATION ? GRADLE_TEST_DEPENDENCY_NEEDLE : GRADLE_DEPENDENCY_NEEDLE ), - "%s%s(libs.%s)".formatted(indentation.times(1), gradleScope.command(), dependencySlug(dependency).replace("-", ".")) + "%s%s(libs.%s)".formatted(indentation.times(1), gradleScope.command(), VersionsCatalog.dependencySlug(dependency).replace("-", ".")) ); fileReplacer.handle(projectFolder, ContentReplacers.of(new MandatoryFileReplacer(new JHipsterProjectFilePath(BUILD_GRADLE_FILE), replacer))); } - private void addJavaDependencyToVersionCatalog(JavaDependency dependency) { - Config libraryConfig = Config.inMemory(); - libraryConfig.set("group", dependency.id().groupId().get()); - libraryConfig.set("name", dependency.id().artifactId().get()); - dependency.version().ifPresent(versionSlug -> libraryConfig.set("version.ref", versionSlug.slug())); - String libraryEntryKey = dependencySlug(dependency); - versionsCatalog.set(List.of(LIBRARIES_TOML_KEY, libraryEntryKey), libraryConfig); - versionsCatalog.save(); - } - - private static String dependencySlug(JavaDependency dependency) { - return dependency.slug().map(DependencySlug::slug).orElse(dependency.id().artifactId().get()); + private static GradleDependencyScope gradleDependencyScope(JavaDependency dependency) { + return switch(dependency.scope()) { + case TEST -> GradleDependencyScope.TEST_IMPLEMENTATION; + case PROVIDED -> GradleDependencyScope.COMPILE_ONLY; + case RUNTIME -> GradleDependencyScope.RUNTIME_ONLY; + default -> GradleDependencyScope.IMPLEMENTATION; + }; } @Override - @ExcludeFromGeneratedCodeCoverage(reason = "Not yet implemented") public void handle(RemoveDirectJavaDependency command) { - throw new NotImplementedException(NOT_YET_IMPLEMENTED); + versionsCatalog.retrieveDependencySlugsFrom(command.dependency()).forEach(this::removeJavaDependencyFromBuildGradle); + versionsCatalog.removeLibrary(command.dependency()); + } + + private void removeJavaDependencyFromBuildGradle(DependencySlug dependencySlug) { + String scopePattern = Stream.of(GradleDependencyScope.values()) + .map(GradleDependencyScope::command) + .collect(Collectors.joining("|", "(", ")")); + Pattern dependencyLinePattern = Pattern.compile( + "^\\s+%s\\(libs\\.%s\\)$".formatted(scopePattern, dependencySlug.slug().replace("-", "\\.")), + Pattern.MULTILINE + ); + MandatoryReplacer replacer = new MandatoryReplacer(new RegexReplacer(always(), dependencyLinePattern), ""); + fileReplacer.handle(projectFolder, ContentReplacers.of(new MandatoryFileReplacer(new JHipsterProjectFilePath(BUILD_GRADLE_FILE), replacer))); } @Override diff --git a/src/main/java/tech/jhipster/lite/module/infrastructure/secondary/javadependency/gradle/VersionsCatalog.java b/src/main/java/tech/jhipster/lite/module/infrastructure/secondary/javadependency/gradle/VersionsCatalog.java new file mode 100644 index 00000000000..831ef03c0f0 --- /dev/null +++ b/src/main/java/tech/jhipster/lite/module/infrastructure/secondary/javadependency/gradle/VersionsCatalog.java @@ -0,0 +1,112 @@ +package tech.jhipster.lite.module.infrastructure.secondary.javadependency.gradle; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; +import java.util.function.Predicate; + +import com.electronwill.nightconfig.core.Config; +import com.electronwill.nightconfig.core.Config.Entry; +import com.electronwill.nightconfig.core.UnmodifiableConfig; +import com.electronwill.nightconfig.core.file.FileConfig; +import com.electronwill.nightconfig.core.io.ParsingException; + +import tech.jhipster.lite.module.domain.javabuild.DependencySlug; +import tech.jhipster.lite.module.domain.javadependency.DependencyId; +import tech.jhipster.lite.module.domain.javadependency.JavaDependency; +import tech.jhipster.lite.module.domain.javadependency.JavaDependencyVersion; +import tech.jhipster.lite.module.domain.properties.JHipsterProjectFolder; + +class VersionsCatalog { + + private static final String VERSIONS_TOML_KEY = "versions"; + private static final String LIBRARIES_TOML_KEY = "libraries"; + + private final FileConfig tomlConfigFile; + + public VersionsCatalog(JHipsterProjectFolder projectFolder) { + Path tomlVersionCatalogFile = tomlVersionCatalogPath(projectFolder); + tomlConfigFile = FileConfig.builder(tomlVersionCatalogFile) + .sync() + .build(); + + // Missing TOML file will be automatically created, but its parent folder should exist + if (!Files.exists(tomlVersionCatalogFile.getParent())) { + tomlVersionCatalogFile.toFile().getParentFile().mkdirs(); + } + + try { + tomlConfigFile.load(); + } catch (ParsingException exception) { + throw new InvalidTomlVersionCatalogException(exception); + } + } + + private static Path tomlVersionCatalogPath(JHipsterProjectFolder projectFolder) { + return projectFolder.filePath("gradle").resolve("libs.versions.toml"); + } + + private void save() { + tomlConfigFile.save(); + } + + public static String dependencySlug(JavaDependency dependency) { + return dependency.slug().map(DependencySlug::slug).orElse(dependency.id().artifactId().get()); + } + + public void setVersion(JavaDependencyVersion javaDependencyVersion) { + tomlConfigFile.set(List.of(VERSIONS_TOML_KEY, javaDependencyVersion.slug().slug()), javaDependencyVersion.version().get()); + save(); + } + + public void addLibrary(JavaDependency dependency) { + Config libraryConfig = Config.inMemory(); + libraryConfig.set("group", dependency.id().groupId().get()); + libraryConfig.set("name", dependency.id().artifactId().get()); + dependency.version().ifPresent(versionSlug -> libraryConfig.set("version.ref", versionSlug.slug())); + String libraryEntryKey = dependencySlug(dependency); + tomlConfigFile.set(List.of(LIBRARIES_TOML_KEY, libraryEntryKey), libraryConfig); + save(); + } + + public void removeLibrary(DependencyId dependency) { + libraryEntriesMatchingDependency(dependency).forEach(libraryConfig -> tomlConfigFile.remove(List.of(LIBRARIES_TOML_KEY, libraryConfig.getKey()))); + save(); + } + + private List libraryEntriesMatchingDependency(DependencyId dependency) { + return tomlConfigFile.entrySet().stream() + .filter(entry -> entry.getKey().equals(LIBRARIES_TOML_KEY)) + .map(Entry::getValue) + .filter(Config.class::isInstance) + .map(Config.class::cast) + .map(Config::entrySet) + .flatMap(Collection::stream) + .filter(groupShouldMatch(dependency)) + .filter(nameShouldMatch(dependency)) + .toList(); + } + + private static Predicate groupShouldMatch(DependencyId dependency) { + return libraryConfig -> { + Object groupProperty = ((Config) libraryConfig.getValue()).get("group"); + return dependency.groupId().get().equals(groupProperty); + }; + } + + private static Predicate nameShouldMatch(DependencyId dependency) { + return libraryConfig -> { + Object groupProperty = ((Config) libraryConfig.getValue()).get("name"); + return dependency.artifactId().get().equals(groupProperty); + }; + } + + public Collection retrieveDependencySlugsFrom(DependencyId dependency) { + return libraryEntriesMatchingDependency(dependency) + .stream() + .map(UnmodifiableConfig.Entry::getKey) + .map(DependencySlug::new) + .toList(); + } +} diff --git a/src/test/java/tech/jhipster/lite/module/infrastructure/secondary/javadependency/gradle/GradleCommandHandlerTest.java b/src/test/java/tech/jhipster/lite/module/infrastructure/secondary/javadependency/gradle/GradleCommandHandlerTest.java index 14d5994e038..18e3e2b046c 100644 --- a/src/test/java/tech/jhipster/lite/module/infrastructure/secondary/javadependency/gradle/GradleCommandHandlerTest.java +++ b/src/test/java/tech/jhipster/lite/module/infrastructure/secondary/javadependency/gradle/GradleCommandHandlerTest.java @@ -182,6 +182,82 @@ void shouldAddTestImplementationDependencyInBuildGradleFileForTestScope() { } } + @Nested + class HandleRemoveDirectJavaDependency { + + private final JHipsterProjectFolder projectFolder = projectFrom("src/test/resources/projects/empty-gradle"); + + @Test + void shouldRemoveEntryInLibrariesSection() { + GradleCommandHandler gradleCommandHandler = new GradleCommandHandler(Indentation.DEFAULT, projectFolder); + gradleCommandHandler.handle(new AddDirectJavaDependency(springBootStarterWebDependency())); + + gradleCommandHandler.handle(new RemoveDirectJavaDependency(springBootStarterWebDependency().id())); + + assertThat(versionCatalogContent(projectFolder)) + .doesNotContain("[libraries.spring-boot-starter-web]") + .doesNotContain(""" + \t\tname = "spring-boot-starter-web" + \t\tgroup = "org.springframework.boot" + """); + } + + @Test + void shouldRemoveDepencyInBuildGradleFile() { + GradleCommandHandler gradleCommandHandler = new GradleCommandHandler(Indentation.DEFAULT, projectFolder); + gradleCommandHandler.handle(new AddDirectJavaDependency(springBootStarterWebDependency())); + + gradleCommandHandler.handle(new RemoveDirectJavaDependency(springBootStarterWebDependency().id())); + + assertThat(buildGradleContent(projectFolder)).doesNotContain("implementation(libs.spring.boot.starter.web)"); + } + + @Test + void shouldRemoveTestDepencyInBuildGradleFile() { + GradleCommandHandler gradleCommandHandler = new GradleCommandHandler(Indentation.DEFAULT, projectFolder); + JavaDependency dependency = javaDependency() + .groupId("org.junit.jupiter") + .artifactId("junit-jupiter-engine") + .scope(JavaDependencyScope.TEST) + .build(); + gradleCommandHandler.handle(new AddDirectJavaDependency(dependency)); + + gradleCommandHandler.handle(new RemoveDirectJavaDependency(dependency.id())); + + assertThat(buildGradleContent(projectFolder)).doesNotContain("testImplementation(libs.junit.jupiter.engine)"); + } + + @Test + void shouldRemoveRuntimeDepencyInBuildGradleFile() { + GradleCommandHandler gradleCommandHandler = new GradleCommandHandler(Indentation.DEFAULT, projectFolder); + JavaDependency dependency = javaDependency() + .groupId("org.junit.jupiter") + .artifactId("junit-jupiter-engine") + .scope(JavaDependencyScope.RUNTIME) + .build(); + gradleCommandHandler.handle(new AddDirectJavaDependency(dependency)); + + gradleCommandHandler.handle(new RemoveDirectJavaDependency(dependency.id())); + + assertThat(buildGradleContent(projectFolder)).doesNotContain("runtimeOnly(libs.junit.jupiter.engine)"); + } + + @Test + void shouldRemoveProvidedDepencyInBuildGradleFile() { + GradleCommandHandler gradleCommandHandler = new GradleCommandHandler(Indentation.DEFAULT, projectFolder); + JavaDependency dependency = javaDependency() + .groupId("org.junit.jupiter") + .artifactId("junit-jupiter-engine") + .scope(JavaDependencyScope.PROVIDED) + .build(); + gradleCommandHandler.handle(new AddDirectJavaDependency(dependency)); + + gradleCommandHandler.handle(new RemoveDirectJavaDependency(dependency.id())); + + assertThat(buildGradleContent(projectFolder)).doesNotContain("compileOnly(libs.junit.jupiter.engine)"); + } + } + private static String buildGradleContent(JHipsterProjectFolder projectFolder) { return content(Paths.get(projectFolder.get()).resolve("build.gradle.kts")); }