diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/cli/mixin/FoDAppOrReleaseArgGroup.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/cli/mixin/FoDAppOrReleaseArgGroup.java new file mode 100644 index 0000000000..850299ece3 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/cli/mixin/FoDAppOrReleaseArgGroup.java @@ -0,0 +1,62 @@ +/* + * Copyright 2021-2026 Open Text. + * + * The only warranties for products and services of Open Text + * and its affiliates and licensors ("Open Text") are as may + * be set forth in the express warranty statements accompanying + * such products and services. Nothing herein should be construed + * as constituting an additional warranty. Open Text shall not be + * liable for technical or editorial errors or omissions contained + * herein. The information contained herein is subject to change + * without notice. + */ +package com.fortify.cli.fod._common.cli.mixin; + +import com.fortify.cli.fod.app.cli.mixin.FoDAppResolverMixin; +import com.fortify.cli.fod.release.cli.mixin.FoDReleaseByQualifiedNameOrIdResolverMixin; + +import lombok.Getter; +import picocli.CommandLine.ArgGroup; +import picocli.CommandLine.Option; + +/** + * Argument group for commands that require either an application or release as + * target. This is a common pattern in FoD commands, where some commands can + * target either an application or a release, and the user can specify either + * one of them. This argument group allows for defining both options in a + * mutually exclusive way, and provides getters for both the app and release + * targets. The app target is resolved using the FoDAppResolverMixin, while the + * release target is resolved using the + * FoDReleaseByQualifiedNameOrIdResolverMixin. This argument group can be used + * in any command that needs to support both app and release targeting, without + * having to duplicate the logic for resolving the targets. + * + * @author Sangamesh Vijaykumar + */ +public final class FoDAppOrReleaseArgGroup { + @ArgGroup(exclusive = false, multiplicity = "1", order = 1) + @Getter private FoDAppArgGroup app = new FoDAppArgGroup(); + + @ArgGroup(exclusive = false, multiplicity = "1", order = 2) + @Getter private FoDReleaseArgGroup release = new FoDReleaseArgGroup(); + + public static class FoDAppArgGroup extends FoDAppResolverMixin.AbstractFoDAppResolverMixin { + @Option( + names = { "--app" }, + required = true, + descriptionKey = "fcli.fod.app.app-name-or-id" + ) + @Getter private String appNameOrId; + } + + public static class FoDReleaseArgGroup + extends FoDReleaseByQualifiedNameOrIdResolverMixin.AbstractFoDQualifiedReleaseNameOrIdResolverMixin { + @Option( + names = { "--release", "--rel" }, + required = true, + paramLabel = "id|app[:ms]:rel", + descriptionKey = "fcli.fod.release.resolver.name-or-id" + ) + @Getter private String qualifiedReleaseNameOrId; + } +} \ No newline at end of file diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/cli/mixin/FoDAppOrReleaseMixin.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/cli/mixin/FoDAppOrReleaseMixin.java new file mode 100644 index 0000000000..4018e8ff00 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/cli/mixin/FoDAppOrReleaseMixin.java @@ -0,0 +1,64 @@ +/* + * Copyright 2021-2026 Open Text. + * + * The only warranties for products and services of Open Text + * and its affiliates and licensors ("Open Text") are as may + * be set forth in the express warranty statements accompanying + * such products and services. Nothing herein should be construed + * as constituting an additional warranty. Open Text shall not be + * liable for technical or editorial errors or omissions contained + * herein. The information contained herein is subject to change + * without notice. + */ +package com.fortify.cli.fod._common.cli.mixin; + +import kong.unirest.UnirestInstance; +import lombok.Getter; +import picocli.CommandLine.ArgGroup; + +/** + * Mixin that allows specifying either an application or a release as target for + * a command. The application and release are specified using the same syntax as + * the corresponding resolvers (FoDAppResolverMixin and + * FoDReleaseByQualifiedNameOrIdResolverMixin, respectively), and the mixin + * ensures that exactly one of them is specified. The mixin also propagates the + * delimiter mixin to the release resolver, so that the same delimiter can be + * used for both application and release specification. This mixin is intended + * to be used in commands that can target either an application or a release, + * and that need to resolve the corresponding IDs based on the specified names + * or IDs. + * + * @author Sangamesh Vijaykumar + */ +public final class FoDAppOrReleaseMixin implements IFoDDelimiterMixinAware { + @ArgGroup(exclusive = true, multiplicity = "1", order = 1) + @Getter private FoDAppOrReleaseArgGroup fodAppOrReleaseArgGroup = new FoDAppOrReleaseArgGroup(); + + @Override + public void setDelimiterMixin(FoDDelimiterMixin delimiterMixin) { + FoDAppOrReleaseArgGroup.FoDReleaseArgGroup release = fodAppOrReleaseArgGroup.getRelease(); + if (release != null) { + release.setDelimiterMixin(delimiterMixin); + } + } + + public boolean isAppSpecified() { + FoDAppOrReleaseArgGroup.FoDAppArgGroup app = fodAppOrReleaseArgGroup.getApp(); + return app != null && app.getAppNameOrId() != null; + } + + public boolean isReleaseSpecified() { + FoDAppOrReleaseArgGroup.FoDReleaseArgGroup release = fodAppOrReleaseArgGroup.getRelease(); + return release != null && release.getQualifiedReleaseNameOrId() != null; + } + + public String getAppId(UnirestInstance unirest) { + FoDAppOrReleaseArgGroup.FoDAppArgGroup app = fodAppOrReleaseArgGroup.getApp(); + return app == null ? null : app.getAppId(unirest); + } + + public String getReleaseId(UnirestInstance unirest) { + FoDAppOrReleaseArgGroup.FoDReleaseArgGroup release = fodAppOrReleaseArgGroup.getRelease(); + return release == null ? null : release.getReleaseId(unirest); + } +} \ No newline at end of file diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueListCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueListCommand.java index c017709fe5..dc60abb40d 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueListCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueListCommand.java @@ -22,7 +22,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fortify.cli.common.exception.FcliSimpleException; import com.fortify.cli.common.json.JsonHelper; import com.fortify.cli.common.json.producer.AbstractObjectNodeProducer.AbstractObjectNodeProducerBuilder; import com.fortify.cli.common.json.producer.IObjectNodeProducer; @@ -32,17 +31,16 @@ import com.fortify.cli.common.rest.query.IServerSideQueryParamGeneratorSupplier; import com.fortify.cli.common.rest.query.IServerSideQueryParamValueGenerator; import com.fortify.cli.common.util.Break; +import com.fortify.cli.fod._common.cli.mixin.FoDAppOrReleaseMixin; import com.fortify.cli.fod._common.cli.mixin.FoDDelimiterMixin; import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDOutputCommand; import com.fortify.cli.fod._common.rest.FoDUrls; import com.fortify.cli.fod._common.rest.query.FoDFiltersParamGenerator; import com.fortify.cli.fod._common.rest.query.cli.mixin.FoDFiltersParamMixin; -import com.fortify.cli.fod.app.cli.mixin.FoDAppResolverMixin; import com.fortify.cli.fod.issue.cli.mixin.FoDIssueEmbedMixin; import com.fortify.cli.fod.issue.cli.mixin.FoDIssueIncludeMixin; import com.fortify.cli.fod.issue.helper.FoDIssueHelper; import com.fortify.cli.fod.issue.helper.FoDIssueHelper.IssueAggregationData; -import com.fortify.cli.fod.release.cli.mixin.FoDReleaseByQualifiedNameOrIdResolverMixin; import com.fortify.cli.fod.release.helper.FoDReleaseDescriptor; import com.fortify.cli.fod.release.helper.FoDReleaseHelper; @@ -56,9 +54,8 @@ @Command(name = OutputHelperMixins.List.CMD_NAME) public class FoDIssueListCommand extends AbstractFoDOutputCommand implements IServerSideQueryParamGeneratorSupplier { @Getter @Mixin private OutputHelperMixins.List outputHelper; - @Mixin private FoDDelimiterMixin delimiterMixin; // injected in resolvers - @Mixin private FoDAppResolverMixin.OptionalOption appResolver; - @Mixin private FoDReleaseByQualifiedNameOrIdResolverMixin.OptionalOption releaseResolver; + @Mixin private FoDDelimiterMixin delimiterMixin; + @Mixin @Getter private FoDAppOrReleaseMixin appOrRelease; @Mixin private FoDFiltersParamMixin filterParamMixin; @Mixin private FoDIssueEmbedMixin embedMixin; @Mixin private FoDIssueIncludeMixin includeMixin; @@ -77,21 +74,10 @@ public class FoDIssueListCommand extends AbstractFoDOutputCommand implements ISe @Override protected IObjectNodeProducer getObjectNodeProducer(UnirestInstance unirest) { - boolean releaseSpecified = releaseResolver.getQualifiedReleaseNameOrId() != null; - boolean appSpecified = appResolver.getAppNameOrId() != null; - if ( releaseSpecified && appSpecified ) { - throw new FcliSimpleException("Cannot specify both an application and release"); - } - if ( !releaseSpecified && !appSpecified ) { - throw new FcliSimpleException("Either an application or release must be specified"); - } + boolean releaseSpecified = appOrRelease.isReleaseSpecified(); var result = releaseSpecified - ? singleReleaseProducerBuilder(unirest, releaseResolver.getReleaseId(unirest)) - : applicationProducerBuilder(unirest, appResolver.getAppId(unirest)); - // For consistent output, we should remove releaseId/releaseName when listing across multiple releases, - // but that breaks existing scripts that may rely on those fields, so for now, we only do this in - // applicationProducerBuilder(). TODO: Change in in fcli v4.0. - // return result.recordTransformer(this::removeReleaseProperties).build(); + ? singleReleaseProducerBuilder(unirest, appOrRelease.getReleaseId(unirest)) + : applicationProducerBuilder(unirest, appOrRelease.getAppId(unirest)); return result.build(); } @@ -225,17 +211,12 @@ private JsonNode enrichIssueRecord(UnirestInstance unirest, String releaseName, } private boolean isEffectiveFastOutput() { - boolean appSpecified = appResolver.getAppNameOrId() != null; - boolean releaseSpecified = releaseResolver.getQualifiedReleaseNameOrId() != null; - if ( !appSpecified || releaseSpecified ) { return false; } + if (!appOrRelease.isAppSpecified() || appOrRelease.isReleaseSpecified()) { + return false; + } boolean fastOutputStyle = outputHelper.getRecordWriterStyle().isFastOutput(); boolean streamingSupported = outputHelper.isStreamingOutputSupported(); - boolean recordConsumerConfigured = getRecordConsumer()!=null; - // Effective fast output requires: - // - application specified (multiple releases) - // - fast output style - // - no aggregation (merging requires full set) - // - streaming output or record consumer configured + boolean recordConsumerConfigured = getRecordConsumer() != null; return fastOutputStyle && !aggregate && (streamingSupported || recordConsumerConfigured); } diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/oss_scan/cli/cmd/FoDOssComponentsListCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/oss_scan/cli/cmd/FoDOssComponentsListCommand.java index 7fb80227dc..53830d51fe 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/oss_scan/cli/cmd/FoDOssComponentsListCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/oss_scan/cli/cmd/FoDOssComponentsListCommand.java @@ -26,6 +26,7 @@ import com.fortify.cli.common.json.JsonHelper; import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; import com.fortify.cli.common.rest.unirest.UnexpectedHttpResponseException; +import com.fortify.cli.fod._common.cli.mixin.FoDAppOrReleaseMixin; import com.fortify.cli.fod._common.cli.mixin.FoDDelimiterMixin; import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod._common.rest.FoDUrls; @@ -33,8 +34,6 @@ import com.fortify.cli.fod._common.rest.helper.FoDPagingHelper; import com.fortify.cli.fod._common.scan.helper.FoDOpenSourceScanType; import com.fortify.cli.fod._common.scan.helper.oss.FoDScanOssHelper; -import com.fortify.cli.fod.app.cli.mixin.FoDAppResolverMixin; -import com.fortify.cli.fod.release.cli.mixin.FoDReleaseByQualifiedNameOrIdResolverMixin; import kong.unirest.HttpResponse; import kong.unirest.UnirestInstance; @@ -47,24 +46,24 @@ @CommandGroup("oss-components") public final class FoDOssComponentsListCommand extends AbstractFoDJsonNodeOutputCommand { private static final Logger LOG = LoggerFactory.getLogger(FoDOssComponentsListCommand.class); - @Getter - @Mixin - private OutputHelperMixins.TableWithQuery outputHelper; - @Mixin - private FoDDelimiterMixin delimiterMixin; // Is automatically injected in resolver mixins - @Mixin - private FoDAppResolverMixin.OptionalOption appResolver; - @Mixin - private FoDReleaseByQualifiedNameOrIdResolverMixin.OptionalOption releaseResolver; - @Option(names = "--scan-types", required = true, split = ",", defaultValue = "Debricked") - private FoDOpenSourceScanType[] scanTypes; + @Getter @Mixin private OutputHelperMixins.TableWithQuery outputHelper; + @Mixin private FoDDelimiterMixin delimiterMixin; + @Mixin @Getter private FoDAppOrReleaseMixin appOrRelease; + @Option(names = "--scan-types", required = true, split = ",", defaultValue = "Debricked") private FoDOpenSourceScanType[] scanTypes; @Override public JsonNode getJsonNode(UnirestInstance unirest) { ArrayNode result = JsonHelper.getObjectMapper().createArrayNode(); + + final String applicationId = appOrRelease.isAppSpecified() + ? appOrRelease.getAppId(unirest) + : null; + final String releaseId = appOrRelease.isReleaseSpecified() + ? appOrRelease.getReleaseId(unirest) + : null; + Stream.of(scanTypes) - .map(t -> getForOpenSourceScanType(unirest, t, releaseResolver.getReleaseId(unirest), - appResolver.getAppId(unirest), false)) + .map(t -> getForOpenSourceScanType(unirest, t, releaseId, applicationId, false)) .forEach(result::addAll); return result; } @@ -100,7 +99,7 @@ private ArrayNode getForOpenSourceScanType(UnirestInstance unirest, FoDOpenSourc if (failOnError) { throw e; } - LOG.error("Error retrieving OSS components for release " + releaseResolver.getReleaseId(unirest) + LOG.error("Error retrieving OSS components for release " + releaseId + " and scan type " + scanType.name() + ": " + e.getMessage()); return JsonHelper.getObjectMapper().createArrayNode(); } diff --git a/fcli-core/fcli-fod/src/test/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueListCommandEffectiveFastOutputTest.java b/fcli-core/fcli-fod/src/test/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueListCommandEffectiveFastOutputTest.java index 1de2141541..a0cff2a4c4 100644 --- a/fcli-core/fcli-fod/src/test/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueListCommandEffectiveFastOutputTest.java +++ b/fcli-core/fcli-fod/src/test/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueListCommandEffectiveFastOutputTest.java @@ -23,8 +23,7 @@ import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; import com.fortify.cli.common.output.writer.record.RecordWriterStyle; import com.fortify.cli.common.output.writer.record.RecordWriterStyle.RecordWriterStyleElement; -import com.fortify.cli.fod.app.cli.mixin.FoDAppResolverMixin; -import com.fortify.cli.fod.release.cli.mixin.FoDReleaseByQualifiedNameOrIdResolverMixin; +import com.fortify.cli.fod._common.cli.mixin.FoDAppOrReleaseMixin; /** * Tests for FoDIssueListCommand.isEffectiveFastOutput logic after migration to style-based fast-output. @@ -39,10 +38,8 @@ public class FoDIssueListCommandEffectiveFastOutputTest { void init() throws Exception { cmd = new FoDIssueListCommand(); streamingStub = new StreamingStubOutputHelper(); - setField(cmd, "outputHelper", streamingStub); // inject stub - // Provide empty mixins so reflection can set their private fields - setField(cmd, "appResolver", new FoDAppResolverMixin.OptionalOption()); - setField(cmd, "releaseResolver", new FoDReleaseByQualifiedNameOrIdResolverMixin.OptionalOption()); + setField(cmd, "outputHelper", streamingStub); + setField(cmd, "appOrRelease", new FoDAppOrReleaseMixin()); } @Test @@ -94,17 +91,17 @@ private void setField(Object target, String fieldName, Object value) throws Exce } private void setApp(String app) throws Exception { - Object appResolver = getField(cmd, "appResolver"); - setField(appResolver, "appNameOrId", app); + var appOrRelease = cmd.getAppOrRelease(); + var target = appOrRelease.getFodAppOrReleaseArgGroup(); + var appGroup = target.getApp(); + setField(appGroup, "appNameOrId", app); } + private void setRelease(String rel) throws Exception { - Object relResolver = getField(cmd, "releaseResolver"); - setField(relResolver, "qualifiedReleaseNameOrId", rel); - } - private Object getField(Object target, String fieldName) throws Exception { - Field f = target.getClass().getDeclaredField(fieldName); - f.setAccessible(true); - return f.get(target); + var appOrRelease = cmd.getAppOrRelease(); + var target = appOrRelease.getFodAppOrReleaseArgGroup(); + var releaseGroup = target.getRelease(); + setField(releaseGroup, "qualifiedReleaseNameOrId", rel); } private boolean invokeIsEffectiveFastOutput() throws Exception {