diff --git a/components/sbm-core/src/main/java/org/springframework/sbm/build/api/DependencyChangeResolver.java b/components/sbm-core/src/main/java/org/springframework/sbm/build/api/DependencyChangeResolver.java
new file mode 100644
index 000000000..510d6e242
--- /dev/null
+++ b/components/sbm-core/src/main/java/org/springframework/sbm/build/api/DependencyChangeResolver.java
@@ -0,0 +1,89 @@
+package org.springframework.sbm.build.api;
+
+import lombok.NonNull;
+import org.apache.commons.lang3.tuple.Pair;
+import org.openrewrite.maven.tree.Scope;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+import static org.openrewrite.maven.tree.Scope.*;
+
+/**
+ * Resolve the dependency change spec. Their is a ascending
+ * order of the scopes where a higher order covers its predecessor
+ * and more. Below is the ascending order of the scopes.
+ *
+ * Test (lowest), Runtime, Provided, Compile (highest)
+ *
+ * Based on the above scope, the following rule decides the fate
+ * of a proposed dependency change spec.
+ *
+ * Rule 1 :- If the proposed dependency already exists
+ * transitively but its scope is lesser than the proposed
+ * scope, the proposed dependency will be added to the
+ * build file.
+ *
+ * Rule 2 :- If the proposed dependency already declared
+ * directly but its scope is lesser than the the proposed
+ * scope, the existing dependency will be replaced.
+ *
+ * Rule 3 :- If there is no matching dependency already exists
+ * the proposed dependency will beadded.
+ */
+public class DependencyChangeResolver {
+
+ public static final EnumSet ALL_SCOPES = EnumSet.range(None, System);
+ @NonNull
+ private BuildFile buildFile;
+ private @NonNull Dependency proposedChangeSpec;
+ private List potentialMatches;
+
+ private static Map> UPGRADE_GRAPH = new HashMap<>();
+
+ static {
+ // For a given scope (key), SBM will upgrade ( upsert) if any of the listed scope
+ // exists in the directly included dependencies
+ UPGRADE_GRAPH.put(Compile, List.of(Test, Provided, Runtime));
+ UPGRADE_GRAPH.put(Provided, List.of(Test, Runtime));
+ UPGRADE_GRAPH.put(Runtime, List.of(Test));
+ UPGRADE_GRAPH.put(Test, Collections.emptyList());
+ }
+
+ public DependencyChangeResolver(BuildFile buildFile, @NonNull Dependency proposedChangeSpec) {
+ this.buildFile = buildFile;
+ this.proposedChangeSpec = proposedChangeSpec;
+ this.potentialMatches = buildFile.getEffectiveDependencies()
+ .stream()
+ .filter(d -> d.equals(this.proposedChangeSpec))
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Return a pair of dependencies to be removed ( left) and added ( right)
+ * @return
+ */
+ public Pair, Optional> apply() {
+ if (potentialMatches.isEmpty())
+ return Pair.of(Collections.emptyList(), Optional.of(proposedChangeSpec));
+
+ Scope proposedDependencyScope = Scope.fromName(proposedChangeSpec.getScope());
+ List supersededScopes = UPGRADE_GRAPH.get(proposedDependencyScope);
+
+ Optional right = potentialMatches
+ .stream()
+ .filter(d -> supersededScopes.contains(fromName(d.getScope())))
+ .findAny()
+ .map(any -> proposedChangeSpec);
+
+ List left = buildFile
+ .getDeclaredDependencies(ALL_SCOPES.toArray(new Scope[0]))
+ .stream()
+ .filter(proposedChangeSpec::equals)
+ .collect(Collectors.toList());
+
+ return Pair.of(left, right);
+
+ }
+
+}
diff --git a/components/sbm-core/src/main/java/org/springframework/sbm/build/migration/actions/AddDependencies.java b/components/sbm-core/src/main/java/org/springframework/sbm/build/migration/actions/AddDependencies.java
index e2d0386f6..af586008b 100644
--- a/components/sbm-core/src/main/java/org/springframework/sbm/build/migration/actions/AddDependencies.java
+++ b/components/sbm-core/src/main/java/org/springframework/sbm/build/migration/actions/AddDependencies.java
@@ -16,8 +16,10 @@
package org.springframework.sbm.build.migration.actions;
import lombok.AllArgsConstructor;
+import org.apache.commons.lang3.tuple.Pair;
import org.springframework.sbm.build.api.BuildFile;
import org.springframework.sbm.build.api.Dependency;
+import org.springframework.sbm.build.api.DependencyChangeResolver;
import org.springframework.sbm.engine.recipe.AbstractAction;
import org.springframework.sbm.engine.context.ProjectContext;
import lombok.Getter;
@@ -29,6 +31,9 @@
import javax.validation.Valid;
import java.util.ArrayList;
import java.util.List;
+import java.util.Optional;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
@Setter
@Getter
@@ -51,6 +56,23 @@ public AddDependencies(List dependencies) {
@Override
public void apply(ProjectContext context) {
BuildFile buildFile = context.getBuildFile();
- buildFile.addDependencies(dependencies);
+ List, Optional>> pairs = dependencies.stream()
+ .map(d -> new DependencyChangeResolver(buildFile, d))
+ .map(DependencyChangeResolver::apply)
+ .collect(Collectors.toList());
+
+ List removeList = pairs.stream()
+ .map(Pair::getLeft)
+ .flatMap(List::stream)
+ .collect(Collectors.toList());
+
+ List addList = pairs.stream()
+ .map(Pair::getRight)
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .collect(Collectors.toList());
+
+ buildFile.removeDependencies(removeList);
+ buildFile.addDependencies(addList);
}
}
diff --git a/components/sbm-core/src/test/java/org/springframework/sbm/build/api/DependencyChangeResolverTest.java b/components/sbm-core/src/test/java/org/springframework/sbm/build/api/DependencyChangeResolverTest.java
new file mode 100644
index 000000000..9b442ea2f
--- /dev/null
+++ b/components/sbm-core/src/test/java/org/springframework/sbm/build/api/DependencyChangeResolverTest.java
@@ -0,0 +1,142 @@
+package org.springframework.sbm.build.api;
+
+import org.apache.commons.lang3.tuple.Pair;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.sbm.build.impl.OpenRewriteMavenBuildFile;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class DependencyChangeResolverTest {
+
+ @Mock
+ OpenRewriteMavenBuildFile buildFile;
+
+ @Test
+ public void givenBuildFile_addNewDependency_withScopeCompile_expectNewDependencyAdded(){
+ Dependency proposedDependency =
+ new Dependency.DependencyBuilder()
+ .groupId("org.springframework.sbm")
+ .artifactId("directDependency")
+ .version("1.0")
+ .scope("compile")
+ .build();
+
+ when(buildFile.getEffectiveDependencies())
+ .thenReturn(Collections.emptySet());
+
+ Pair, Optional> pair =
+ new DependencyChangeResolver(buildFile, proposedDependency).apply();
+
+ assertThat(pair.getLeft().isEmpty());
+ assertThat(pair.getRight().isPresent());
+ assertThat(pair.getRight().get().equals(proposedDependency));
+ }
+
+ @Test
+ public void givenBuildFile_addExistingTransitiveDependency_withLowerScope_expectNoOp(){
+ Dependency proposedDependency =
+ new Dependency.DependencyBuilder()
+ .groupId("org.springframework.sbm")
+ .artifactId("directDependency")
+ .version("1.0")
+ .scope("test")
+ .build();
+
+ Dependency existingDependency =
+ new Dependency.DependencyBuilder()
+ .groupId("org.springframework.sbm")
+ .artifactId("directDependency")
+ .version("1.0")
+ .scope("compile")
+ .build();
+
+ when(buildFile.getEffectiveDependencies())
+ .thenReturn(Set.of(existingDependency));
+
+ when(buildFile.getDeclaredDependencies(any()))
+ .thenReturn(Collections.emptyList());
+
+ Pair, Optional> pair =
+ new DependencyChangeResolver(buildFile, proposedDependency).apply();
+
+ assertThat(pair.getLeft().isEmpty());
+ assertThat(pair.getRight().isEmpty());
+ }
+
+ @Test
+ public void givenBuildFile_addExistingTransitiveDependency_withHigherScope_expectNoOp(){
+ Dependency proposedDependency =
+ new Dependency.DependencyBuilder()
+ .groupId("org.springframework.sbm")
+ .artifactId("directDependency")
+ .version("1.0")
+ .scope("compile")
+ .build();
+
+ Dependency existingDependency =
+ new Dependency.DependencyBuilder()
+ .groupId("org.springframework.sbm")
+ .artifactId("directDependency")
+ .version("1.0")
+ .scope("test")
+ .build();
+
+ when(buildFile.getEffectiveDependencies())
+ .thenReturn(Set.of(existingDependency));
+
+ when(buildFile.getDeclaredDependencies(any()))
+ .thenReturn(Collections.emptyList());
+
+ Pair, Optional> pair =
+ new DependencyChangeResolver(buildFile, proposedDependency).apply();
+
+ assertThat(pair.getLeft().isEmpty());
+ assertThat(pair.getRight().isPresent());
+ assertThat(pair.getRight().get().equals(proposedDependency));
+ }
+
+ @Test
+ public void givenBuildFile_addExistingDirectDependency_withHigherScope_expectNoOp(){
+ Dependency proposedDependency =
+ new Dependency.DependencyBuilder()
+ .groupId("org.springframework.sbm")
+ .artifactId("directDependency")
+ .version("1.0")
+ .scope("compile")
+ .build();
+
+ Dependency existingDependency =
+ new Dependency.DependencyBuilder()
+ .groupId("org.springframework.sbm")
+ .artifactId("directDependency")
+ .version("1.0")
+ .scope("test")
+ .build();
+
+ when(buildFile.getEffectiveDependencies())
+ .thenReturn(Set.of(existingDependency));
+
+ when(buildFile.getDeclaredDependencies(any()))
+ .thenReturn(List.of(existingDependency));
+
+ Pair, Optional> pair =
+ new DependencyChangeResolver(buildFile, proposedDependency).apply();
+
+ assertThat(!pair.getLeft().isEmpty());
+ assertThat(pair.getLeft().get(0).getScope().equals("test"));
+ assertThat(pair.getRight().isPresent());
+ assertThat(pair.getRight().get().getScope().equals("compile"));
+ }
+
+}