Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -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;
Expand All @@ -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();
}

Expand Down Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,14 @@
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;
import com.fortify.cli.fod._common.rest.helper.FoDInputTransformer;
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;
Expand All @@ -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;
}
Expand Down Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
Loading