diff --git a/src/main/java/org/wildfly/prospero/extras/Main.java b/src/main/java/org/wildfly/prospero/extras/Main.java index 66af726..eff6b70 100644 --- a/src/main/java/org/wildfly/prospero/extras/Main.java +++ b/src/main/java/org/wildfly/prospero/extras/Main.java @@ -24,6 +24,7 @@ import org.wildfly.prospero.extras.manifest.diff.ManifestsDiffCommand; import org.wildfly.prospero.extras.manifest.download.DownloadDiffCommand; import org.wildfly.prospero.extras.manifest.merge.ManifestMergeCommand; +import org.wildfly.prospero.extras.manifest.subtract.ManifestSubtractCommand; import org.wildfly.prospero.extras.repoository.RepositoryCommands; import org.wildfly.prospero.extras.repository.create.DownloadArtifactListCommand; import org.wildfly.prospero.extras.repository.create.DownloadRepositoryCommand; @@ -44,6 +45,7 @@ private static CommandLine createCommandLine() { commandLine.addSubcommand(new ManifestsDiffCommand()); commandLine.addSubcommand(new DownloadDiffCommand()); commandLine.addSubcommand(new ManifestMergeCommand()); + commandLine.addSubcommand(new ManifestSubtractCommand()); commandLine.addSubcommand(new DownloadRepositoryCommand()); commandLine.addSubcommand(new DownloadArtifactListCommand()); diff --git a/src/main/java/org/wildfly/prospero/extras/manifest/ManifestOperations.java b/src/main/java/org/wildfly/prospero/extras/manifest/ManifestOperations.java index 550bfbd..d40ed32 100644 --- a/src/main/java/org/wildfly/prospero/extras/manifest/ManifestOperations.java +++ b/src/main/java/org/wildfly/prospero/extras/manifest/ManifestOperations.java @@ -28,6 +28,21 @@ ChannelManifest merge(ChannelManifest manifestOne, ChannelManifest manifestTwo, VersionMergeStrategy.Strategies mergeStrategy, String mergedManifestName, String mergedManifestId); + /** + * Subtracts streams of two manifests. + * + * Returns a manifest containing only streams from the first manifest that are The versions of artifacts in streams are ignored. + * + * The excluded streams are always included in the output manifest even if they are present in the second manifest. + * + * @param manifestOne - Initial manifest that the streams will be removed from. + * @param manifestTwo - Manifest containing streams to be removed. + * @param exclusions - list of excluded streams. To include all streams matching a group a wildcard syntax. + * @return + */ + ChannelManifest subtract(ChannelManifest manifestOne, ChannelManifest manifestTwo, + List exclusions); + /** * Performs a diff of {@code manifestOne} and {@code manifestTwo}. * diff --git a/src/main/java/org/wildfly/prospero/extras/manifest/ManifestOperationsImpl.java b/src/main/java/org/wildfly/prospero/extras/manifest/ManifestOperationsImpl.java index d4e8a6a..c1f757e 100644 --- a/src/main/java/org/wildfly/prospero/extras/manifest/ManifestOperationsImpl.java +++ b/src/main/java/org/wildfly/prospero/extras/manifest/ManifestOperationsImpl.java @@ -5,6 +5,7 @@ import org.wildfly.prospero.extras.manifest.diff.ManifestsDiffCommand; import org.wildfly.prospero.extras.manifest.merge.ManifestMergeCommand; import org.wildfly.prospero.extras.manifest.merge.VersionMergeStrategy; +import org.wildfly.prospero.extras.manifest.subtract.ManifestSubtractCommand; import java.util.List; @@ -17,6 +18,12 @@ public ChannelManifest merge(ChannelManifest manifestOne, ChannelManifest manife return ManifestMergeCommand.merge(manifestOne, manifestTwo, mergeStrategy, mergedManifestName, mergedManifestId); } + @Override + public ChannelManifest subtract(ChannelManifest manifestOne, ChannelManifest manifestTwo, + List exclusions) { + return ManifestSubtractCommand.subtract(manifestOne, manifestTwo, exclusions); + } + @Override public List diff(ChannelManifest manifestOne, ChannelManifest manifestTwo) { return ManifestsDiffCommand.manifestDiff(manifestOne, manifestTwo); diff --git a/src/main/java/org/wildfly/prospero/extras/manifest/subtract/ManifestSubtractCommand.java b/src/main/java/org/wildfly/prospero/extras/manifest/subtract/ManifestSubtractCommand.java new file mode 100644 index 0000000..36d3b30 --- /dev/null +++ b/src/main/java/org/wildfly/prospero/extras/manifest/subtract/ManifestSubtractCommand.java @@ -0,0 +1,87 @@ +package org.wildfly.prospero.extras.manifest.subtract; + +import org.wildfly.channel.ChannelManifest; +import org.wildfly.channel.ChannelManifestMapper; +import org.wildfly.channel.Stream; +import org.wildfly.prospero.extras.ReturnCodes; +import org.wildfly.prospero.extras.shared.CommandWithHelp; +import picocli.CommandLine; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +@CommandLine.Command(name = "manifest-subtract") +public class ManifestSubtractCommand extends CommandWithHelp { + + private static final Pattern EXCLUSION_PATTERN = Pattern.compile("[\\w-_.*]*:[\\w-_.*]*"); + + @CommandLine.Parameters(index = "0", descriptionKey = "parameterOne") + Path manifestOne; + + @CommandLine.Parameters(index = "1", descriptionKey = "parameterTwo") + Path manifestTwo; + + @CommandLine.Option(names = "--exclude", split = ",") + List exclusions = Collections.emptyList(); + + @Override + public Integer call() throws Exception { + final ChannelManifest manifestOne = ChannelManifestMapper.from(this.manifestOne.toUri().toURL()); + final ChannelManifest manifestTwo = ChannelManifestMapper.from(this.manifestTwo.toUri().toURL()); + + final ChannelManifest res = subtract(manifestOne, manifestTwo, exclusions); + + System.out.println(ChannelManifestMapper.toYaml(res)); + + return ReturnCodes.SUCCESS; + } + + public static ChannelManifest subtract(ChannelManifest manifestOne, ChannelManifest manifestTwo, List exclusions) { + Objects.requireNonNull(manifestOne); + Objects.requireNonNull(manifestTwo); + Objects.requireNonNull(exclusions); + + final Optional illegalPattern = exclusions.stream() + .filter(e -> !EXCLUSION_PATTERN.matcher(e).matches()) + .findFirst(); + if (illegalPattern.isPresent()) { + throw new IllegalArgumentException("Exclusion [" + illegalPattern.get() + "] has invalid format ([\\w-_.*]*:[\\w-_.*]*)."); + } + + final Set groupExclusions = exclusions.stream() + .map(String::trim) + .filter(e -> e.endsWith(":*")) + .map(e -> e.substring(0, e.length() - 2)) + .collect(Collectors.toSet()); + + final Set groupArtifactExclusions = exclusions.stream() + .map(String::trim) + .filter(e -> !e.endsWith(":*")) + .collect(Collectors.toSet()); + + final Set streamKeys = manifestTwo.getStreams().stream() + .filter(s -> !groupExclusions.contains(s.getGroupId())) + .filter(s -> !groupArtifactExclusions.contains(s.getGroupId() + ":" + s.getArtifactId())) + .map(s -> getKey(s)) + .collect(Collectors.toSet()); + + + final List filteredStreams = manifestOne.getStreams().stream() + .filter(s -> !streamKeys.contains(getKey(s))) + .collect(Collectors.toList()); + + + return new ChannelManifest(manifestOne.getSchemaVersion(), manifestOne.getName(), manifestOne.getId(), + manifestOne.getDescription(), manifestOne.getManifestRequirements(), filteredStreams); + } + + private static String getKey(Stream s) { + return s.getGroupId() + ":" + s.getArtifactId(); + } +} diff --git a/src/main/resources/UsageMessages.properties b/src/main/resources/UsageMessages.properties index 37d57f3..e912774 100644 --- a/src/main/resources/UsageMessages.properties +++ b/src/main/resources/UsageMessages.properties @@ -17,6 +17,15 @@ mode=merge strategy to use. The default strategy is ${DEFAULT-VALUE}. tools.manifest-merge.name=name to set in the merged manifest. If not set, defaults to "merged-manifest". tools.manifest-merge.id=id to set in the merged manifest +tools.manifest-subtract.usage.header=Subtracts streams of two manifests. +tools.manifest-subtract.usage.description=Prints a manifest containing only streams from the first manifest that are \ + The versions of artifacts in streams are ignored. +tools.manifest-subtract.exclude=Comma-separated list of excluded streams. The excluded streams are always included in \ + the output manifest even if they are present in the second manifest. To include all streams matching a group a wildcard \ + syntax can be used: @|bold :*|@ +tools.manifest-subtract.parameterOne=Initial manifest that the streams will be removed from. +tools.manifest-subtract.parameterTwo=Manifest containing streams to be removed. + tools.channel.merge-repositories.usage.header=Merges repositories from multiple channels into one channel. tools.channel.merge-repositories.usage.description.0=Prints a channel using all the repositories from the input channels.\ The new channel uses a manifest provided as @|bold manifestUrl|@. diff --git a/src/test/java/org/wildfly/prospero/extras/manifest/subtract/SubtractCommandTest.java b/src/test/java/org/wildfly/prospero/extras/manifest/subtract/SubtractCommandTest.java new file mode 100644 index 0000000..3c27bf0 --- /dev/null +++ b/src/test/java/org/wildfly/prospero/extras/manifest/subtract/SubtractCommandTest.java @@ -0,0 +1,127 @@ +package org.wildfly.prospero.extras.manifest.subtract; + +import org.junit.jupiter.api.Test; +import org.wildfly.channel.ChannelManifest; +import org.wildfly.channel.Stream; + +import java.util.Arrays; +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class SubtractCommandTest { + + @Test + public void testSubtractTwoEmptySetsProducesEmptySet() throws Exception { + final ChannelManifest res = callSubtract(new ChannelManifest("", "", "", Collections.emptyList()), getChannelManifest()); + + assertThat(res.getStreams()) + .isEmpty(); + } + + @Test + public void testSubtractEmptySetProducesEmptySet() throws Exception { + final ChannelManifest c1 = getChannelManifest(new Stream("foo", "bar", "1.1")); + final ChannelManifest res = callSubtract(c1, getChannelManifest()); + + assertThat(res.getStreams()) + .containsAll(c1.getStreams()); + } + + @Test + public void testSubtractFromEmptySetProducesOriginalSet() throws Exception { + final ChannelManifest c1 = getChannelManifest(); + final ChannelManifest c2 = getChannelManifest(new Stream("foo", "bar", "1.1")); + final ChannelManifest res = callSubtract(c1, c2); + + assertThat(res.getStreams()) + .isEmpty(); + } + + @Test + public void testRemoveCommonStream() { + final ChannelManifest c1 = getChannelManifest(new Stream("foo", "bar", "1.1")); + final ChannelManifest c2 = getChannelManifest(new Stream("foo", "bar", "1.1")); + final ChannelManifest res = callSubtract(c1, c2); + + assertThat(res.getStreams()) + .isEmpty(); + } + + @Test + public void testRemoveCommonStreamLivesUnique() { + final ChannelManifest c1 = getChannelManifest( + new Stream("foo", "bar", "1.1"), + new Stream("unique", "one", "1.1") + ); + final ChannelManifest c2 = getChannelManifest(new Stream("foo", "bar", "1.1")); + final ChannelManifest res = callSubtract(c1, c2); + + assertThat(res.getStreams()) + .containsOnly(new Stream("unique", "one", "1.1")); + } + + @Test + public void testRemoveCommonStreamIgnoresVersion() { + final ChannelManifest c1 = getChannelManifest(new Stream("foo", "bar", "1.1")); + final ChannelManifest c2 = getChannelManifest(new Stream("foo", "bar", "1.2")); + final ChannelManifest res = callSubtract(c1, c2); + + assertThat(res.getStreams()) + .isEmpty(); + } + + @Test + public void testExcludeStreamsByGroupArtifact() { + final ChannelManifest c1 = getChannelManifest( + new Stream("foo", "bar", "1.1"), + new Stream("other", "one", "1.1")); + final ChannelManifest c2 = getChannelManifest(new Stream("foo", "bar", "1.1")); + final ChannelManifest res = callSubtract(c1, c2, "foo:bar"); + + assertThat(res.getStreams()) + .containsAll(c1.getStreams()); + } + + @Test + public void testExcludeStreamsByGroup() { + final ChannelManifest c1 = getChannelManifest( + new Stream("foo", "bar", "1.1"), + new Stream("foo", "other", "1.1")); + final ChannelManifest c2 = getChannelManifest( + new Stream("foo", "bar", "1.1"), + new Stream("foo", "other", "1.1") + ); + final ChannelManifest res = callSubtract(c1, c2, "foo:*"); + + assertThat(res.getStreams()) + .containsAll(c1.getStreams()); + } + + @Test + public void testInvalidExclusionPattern() { + final ChannelManifest c1 = getChannelManifest(); + final ChannelManifest c2 = getChannelManifest(); + + assertThatThrownBy(()->callSubtract(c1, c2, "foo")) + .isInstanceOf(IllegalArgumentException.class); + + assertThatThrownBy(()->callSubtract(c1, c2, "foo:bar:aaa")) + .isInstanceOf(IllegalArgumentException.class); + + callSubtract(c1, c2, "org.test:bar-aaa122"); + } + + private static ChannelManifest callSubtract(ChannelManifest c1, ChannelManifest c2, String ... exclusions) { + return ManifestSubtractCommand.subtract(c1, c2, Arrays.asList(exclusions)); + } + + private static ChannelManifest getChannelManifest() { + return new ChannelManifest("", "", "", Collections.emptyList()); + } + + private static ChannelManifest getChannelManifest(Stream... stream) { + return new ChannelManifest("", "", "", Arrays.asList(stream)); + } +} \ No newline at end of file