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
Expand Up @@ -56,6 +56,7 @@ public class ActionStepProcessorRunFcli extends AbstractActionStepProcessorMapEn
private static final String FMT_DEPENDENCY_SKIP_REASON = "%s.dependencySkipReason";
private static final String FMT_EXIT_CODE = "%s.exitCode";
private static final String FMT_RECORDS = "%s.records";
private static final String FMT_METADATA = "%s.metadata";
private static final String FMT_STDOUT = "%s.stdout";
private static final String FMT_STDERR = "%s.stderr";

Expand Down Expand Up @@ -95,18 +96,21 @@ protected final void process(String name, ActionStepRunFcliEntry entry) {
private FcliCommandExecutor createCmdExecutor(ActionStepRunFcliEntry entry, String cmd, FcliRecordConsumer recordConsumer) {
var stdoutOutputType = getOutputTypeOrDefault(entry.getStdoutOutputType(), recordConsumer==null ? OutputType.show : OutputType.suppress );
var stderrOutputType = getOutputTypeOrDefault(entry.getStderrOutputType(), OutputType.show );
ObjectNode[] metadataHolder = {null};
try {
return FcliCommandExecutorFactory.builder()
.cmd(cmd)
.progressOptionValueIfNotPresent(ctx.getConfig().getProgressWriter().type())
.defaultOptionsIfNotPresent(ctx.getConfig().getDefaultFcliRunOptions())
.stdoutOutputType(stdoutOutputType)
.stderrOutputType(stderrOutputType)
.onResult(r->onFcliResult(entry, recordConsumer, r))
.onResult(r->onFcliResult(entry, recordConsumer, metadataHolder, r))
.onSuccess(r->onFcliSuccess(entry))
.onException(e->onFcliException(entry, e))
.onFail(r->onFcliFail(entry, recordConsumer, r))
.recordConsumer(recordConsumer).build().create();
.recordConsumer(recordConsumer)
.metadataConsumer(m->metadataHolder[0]=m)
.build().create();
} catch ( Exception e ) {
onFcliException(entry, e);
return null;
Expand Down Expand Up @@ -205,14 +209,15 @@ private void onFcliException(ActionStepRunFcliEntry fcli, Throwable t) {
}
}

private void onFcliResult(ActionStepRunFcliEntry fcli, FcliRecordConsumer recordConsumer, Result result) {
setResultVars(fcli, recordConsumer, result);
private void onFcliResult(ActionStepRunFcliEntry fcli, FcliRecordConsumer recordConsumer, ObjectNode[] metadataHolder, Result result) {
setResultVars(fcli, recordConsumer, metadataHolder, result);
logStatus(fcli, result);
}

private void setResultVars(ActionStepRunFcliEntry fcli, FcliRecordConsumer recordConsumer, Result result) {
private void setResultVars(ActionStepRunFcliEntry fcli, FcliRecordConsumer recordConsumer, ObjectNode[] metadataHolder, Result result) {
var name = fcli.getKey();
vars.set(String.format(FMT_RECORDS, name), recordConsumer!=null ? recordConsumer.getRecords() : JsonHelper.getObjectMapper().createArrayNode());
vars.set(String.format(FMT_METADATA, name), metadataHolder[0]!=null ? metadataHolder[0] : NullNode.instance);
vars.set(String.format(FMT_STDOUT, name), result.getOut());
vars.set(String.format(FMT_STDERR, name), result.getErr());
vars.set(String.format(FMT_EXIT_CODE, name), new IntNode(result.getExitCode()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
public final class FcliCommandExecutorFactory {
@NonNull private final String cmd;
private final Consumer<ObjectNode> recordConsumer;
private final Consumer<ObjectNode> metadataConsumer;
@Builder.Default private final OutputType stdoutOutputType = OutputType.show;
@Builder.Default private final OutputType stderrOutputType = OutputType.show;
private final Consumer<Result> onResult; // Always executed if fcli command didn't throw exception
Expand Down Expand Up @@ -105,7 +106,7 @@ private int handleParseException(String[] resolvedArgs, ParameterException e) {

public final Result execute() {
if ( parseErrorResult!=null ) { return parseErrorResult; }
if ( recordConsumer!=null && canCollectRecords() ) { setPerCommandRecordConsumer(); }
if ( (recordConsumer!=null || metadataConsumer!=null) && canCollectRecords() ) { setPerCommandConsumers(); }
return call(()->_execute());
}

Expand Down Expand Up @@ -189,10 +190,15 @@ public final boolean canCollectRecords() {
return FcliCommandSpecHelper.canCollectRecords(replicatedLeafCommandSpec);
}

private void setPerCommandRecordConsumer() {
private void setPerCommandConsumers() {
var userObj = replicatedLeafCommandSpec.userObject();
if ( userObj instanceof IRecordCollectionSupport ) {
((IRecordCollectionSupport)userObj).setRecordConsumer(recordConsumer, stdoutOutputType!=OutputType.show);
if ( userObj instanceof IRecordCollectionSupport rcs ) {
if ( recordConsumer!=null ) {
rcs.setRecordConsumer(recordConsumer, stdoutOutputType!=OutputType.show);
}
if ( metadataConsumer!=null ) {
rcs.setMetadataConsumer(metadataConsumer);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.fortify.cli.common.exception.FcliBugException;
import com.fortify.cli.common.json.transform.fields.AddFieldsTransformer;
import com.fortify.cli.common.output.product.IProductHelper;
import com.fortify.cli.common.output.product.IResponseMetadataCollector;
import com.fortify.cli.common.output.transform.IActionCommandResultSupplier;
import com.fortify.cli.common.output.transform.IInputTransformer;
import com.fortify.cli.common.output.transform.IRecordTransformer;
Expand All @@ -50,12 +51,18 @@ public abstract class AbstractObjectNodeProducer implements IObjectNodeProducer
@Getter @Singular private final List<UnaryOperator<JsonNode>> inputTransformers;
@Getter @Singular private final List<UnaryOperator<JsonNode>> recordTransformers;
@Getter private final QueryExpression queryExpression;
@Getter private final IResponseMetadataCollector responseMetadataCollector;
private ObjectNode responseMetadata;

@Override
public ObjectNode getResponseMetadata() { return responseMetadata; }

/**
* Template method used by subclasses to feed input JSON to this base class for processing.
*/
protected final void process(JsonNode input, IObjectNodeConsumer consumer) {
if ( input==null ) { return; }
collectMetadata(input);
JsonNode transformed = applyInputTransformers(input);
if ( transformed==null || transformed.isNull() ) { return; }
if ( transformed.isObject() ) {
Expand Down Expand Up @@ -83,6 +90,12 @@ private JsonNode applyInputTransformers(JsonNode input) {
return current;
}

private void collectMetadata(JsonNode rawInput) {
if ( responseMetadata==null && responseMetadataCollector!=null ) {
responseMetadata = responseMetadataCollector.collectResponseMetadata(rawInput);
}
}

protected Break processSingleRecord(ObjectNode node, IObjectNodeConsumer consumer) {
ObjectNode current = node;
for ( var t : recordTransformers ) {
Expand Down Expand Up @@ -136,6 +149,7 @@ public B applyAllFrom(ObjectNodeProducerApplyFrom applyFrom) {
applyRecordTransformationsFrom(applyFrom);
applyQueryFrom(applyFrom);
applyActionCommandResultSupplierFrom(applyFrom);
applyResponseMetadataCollectorFrom(applyFrom);
return self();
}
private void applyActionCommandResultSupplierFrom(ObjectNodeProducerApplyFrom applyFrom) {
Expand All @@ -158,6 +172,10 @@ public B applyQueryFrom(ObjectNodeProducerApplyFrom applyFrom) {
}
return self();
}
public B applyResponseMetadataCollectorFrom(ObjectNodeProducerApplyFrom applyFrom) {
applyFrom.getSourceStream(getRequiredCommandHelper(), explicitProductHelper).forEach(this::addResponseMetadataCollectorFromObject);
return self();
}
protected ICommandHelper getRequiredCommandHelper() {
if ( commandHelper==null ) {
throw new FcliBugException("CommandHelper not configured; call commandHelper(<helper>) before apply*From()");
Expand All @@ -173,5 +191,10 @@ private void addInputTransformersFromObject(Object o) {
private void addRecordTransformersFromObject(Object o) {
if ( o instanceof IRecordTransformer rt ) { recordTransformer(n->rt.transformRecord(n)); }
}
private void addResponseMetadataCollectorFromObject(Object o) {
if ( o instanceof IResponseMetadataCollector rmc && this.responseMetadataCollector==null ) {
responseMetadataCollector(rmc);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@
public interface IObjectNodeProducer {
void forEach(IObjectNodeConsumer consumer);

/**
* Return response-level metadata (e.g., total count, paging links) captured from
* the underlying data source. Available after {@link #forEach} has been invoked.
* Returns {@code null} if no metadata was captured.
*/
default ObjectNode getResponseMetadata() { return null; }

@FunctionalInterface
interface IObjectNodeConsumer { Break accept(ObjectNode node); }
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import com.fortify.cli.common.rest.paging.INextPageRequestProducer;
import com.fortify.cli.common.rest.paging.INextPageUrlProducer;
import com.fortify.cli.common.rest.paging.INextPageUrlProducerSupplier;
import com.fortify.cli.common.rest.paging.IPagingSuppressor;
import com.fortify.cli.common.rest.paging.PagingHelper;
import com.fortify.cli.common.rest.unirest.IHttpRequestUpdater;
import com.fortify.cli.common.rest.unirest.IUnirestInstanceSupplier;
Expand All @@ -41,6 +42,7 @@ public class RequestObjectNodeProducer extends AbstractObjectNodeProducer {
@Singular private final List<IHttpRequestUpdater> requestUpdaters;
private final INextPageRequestProducer nextPageRequestProducer;
private final INextPageUrlProducer nextPageUrlProducer;
private final boolean pagingSuppressed;
// Test-only support: if configured, simulate multi-page responses without performing HTTP requests
@Singular private final List<JsonNode> testPageBodies;

Expand All @@ -52,6 +54,10 @@ public void forEach(IObjectNodeConsumer consumer) {
return;
}
HttpRequest<?> request = applyRequestUpdaters(baseRequest);
if ( pagingSuppressed ) {
request.asObject(JsonNode.class).ifSuccess(r->handleResponse(r, consumer)).ifFailure(IfFailureHandler::handle);
return;
}
INextPageRequestProducer effectiveNextPageRequestProducer = nextPageRequestProducer;
if ( effectiveNextPageRequestProducer==null && nextPageUrlProducer!=null && unirestInstance!=null ) {
effectiveNextPageRequestProducer = PagingHelper.asNextPageRequestProducer(unirestInstance, nextPageUrlProducer);
Expand Down Expand Up @@ -83,29 +89,14 @@ public RequestObjectNodeProducerBuilderImpl applyAllFrom(ObjectNodeProducerApply
var ch = getRequiredCommandHelper();
ch.getCommandAs(IUnirestInstanceSupplier.class)
.ifPresent(s -> super.unirestInstance(s.getUnirestInstance()));
applyRequestUpdatersFrom(applyFrom);
applyNextPageUrlProducerFrom(applyFrom);
applyFrom.getSourceStream(ch, getExplicitProductHelper()).forEach(this::applyFromObject);
return self();
}

private void applyNextPageUrlProducerFrom(ObjectNodeProducerApplyFrom applyFrom) {
applyFrom.getSourceStream(getRequiredCommandHelper(), getExplicitProductHelper()).forEach(this::addNextPageUrlProducerFromObject);
}

public void applyRequestUpdatersFrom(ObjectNodeProducerApplyFrom applyFrom) {
applyFrom.getSourceStream(getRequiredCommandHelper(), getExplicitProductHelper()).forEach(this::addRequestUpdaterFromObject);
}

private void addRequestUpdaterFromObject(Object o) {
if (o instanceof IHttpRequestUpdater u) {
requestUpdater(u);
}
}

private void addNextPageUrlProducerFromObject(Object o) {
if (o instanceof INextPageUrlProducerSupplier s) {
nextPageUrlProducer(s.getNextPageUrlProducer());
}
private void applyFromObject(Object o) {
if (o instanceof IHttpRequestUpdater u) { requestUpdater(u); }
if (o instanceof INextPageUrlProducerSupplier s) { nextPageUrlProducer(s.getNextPageUrlProducer()); }
if (o instanceof IPagingSuppressor s && s.isPagingSuppressed()) { pagingSuppressed(true); }
}

/** Configure unirest instance to enable streaming paging conversion. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public abstract class AbstractOutputCommand extends AbstractRunnableCommand
implements ISingularSupplier, IOutputHelperSupplier, IRecordCollectionSupport
{
@Getter private Consumer<ObjectNode> recordConsumer;
@Getter private Consumer<ObjectNode> metadataConsumer;
@Getter private boolean stdoutSuppressedForRecordCollection;

@Override
Expand Down Expand Up @@ -125,4 +126,9 @@ public final void setRecordConsumer(Consumer<ObjectNode> consumer, boolean suppr
this.recordConsumer = consumer;
this.stdoutSuppressedForRecordCollection = suppressStdout;
}

@Override
public final void setMetadataConsumer(Consumer<ObjectNode> consumer) {
this.metadataConsumer = consumer;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,6 @@ public interface IRecordCollectionSupport {
void setRecordConsumer(Consumer<ObjectNode> consumer, boolean suppressStdout);
Consumer<ObjectNode> getRecordConsumer();
boolean isStdoutSuppressedForRecordCollection();
default void setMetadataConsumer(Consumer<ObjectNode> consumer) {}
default Consumer<ObjectNode> getMetadataConsumer() { return null; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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.common.output.product;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

/**
* Interface for extracting response-level metadata (e.g., total record count,
* paging links) from a raw API response body before input transformation
* strips non-record data.
* <p>
* Product helpers (e.g., SSCProductHelper, FoDProductHelper) implement this
* to capture metadata specific to their API response format.
*/
@FunctionalInterface
public interface IResponseMetadataCollector {
/**
* Extract metadata from the raw response body. Called before input
* transformation (which typically extracts only the records array).
* Returns {@code null} if no metadata is available.
*/
ObjectNode collectResponseMetadata(JsonNode responseBody);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import java.io.FileWriter;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.function.Consumer;

import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fortify.cli.common.cli.util.FcliCommandSpecHelper;
Expand Down Expand Up @@ -51,6 +52,7 @@ public class StandardOutputWriter implements IOutputWriter {
private final IOutputOptions outputOptions;
private final IMessageResolver messageResolver;
private final IRecordWriter recordCollector; // instance-level collector, may be null
private final Consumer<ObjectNode> metadataConsumer; // programmatic metadata callback, may be null
private final boolean suppressOutput;

public StandardOutputWriter(CommandSpec commandSpec, IOutputOptions outputOptions, StandardOutputConfig defaultOutputConfig) {
Expand All @@ -72,9 +74,11 @@ public void close() {
}
};
this.suppressOutput = rcs.isStdoutSuppressedForRecordCollection();
this.metadataConsumer = rcs.getMetadataConsumer();
} else {
this.recordCollector = null;
this.suppressOutput = false;
this.metadataConsumer = null;
}
}

Expand All @@ -89,6 +93,11 @@ public void write(IObjectNodeProducer recordProducer) {
}
try (IRecordWriter rw = new OutputAndVariableRecordWriter()) {
recordProducer.forEach(recordConsumer(rw));
var metadata = recordProducer.getResponseMetadata();
rw.setResponseMetadata(metadata);
if (metadataConsumer != null && metadata != null) {
metadataConsumer.accept(metadata);
}
}
}

Expand Down Expand Up @@ -148,6 +157,13 @@ public void close() {
}
}

@Override
public void setResponseMetadata(ObjectNode metadata) {
if (outputRecordWriter != null) {
outputRecordWriter.setResponseMetadata(metadata);
}
}

private IRecordWriter createOutputRecordWriter() {
return suppressOutput ? null : createUnsuppressed();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,10 @@
public interface IRecordWriter extends Closeable {
void append(ObjectNode node);
void close();
/**
* Set response-level metadata to be included in the output when envelope
* style is active. Must be called before {@link #close()} for the metadata
* to be written. Writers that do not support envelope style ignore this.
*/
default void setResponseMetadata(ObjectNode metadata) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ public final boolean isFastOutput() {
return getOrDefault(RecordWriterStyleElementGroup.FAST_OUTPUT)==RecordWriterStyleElement.fast_output;
}

/** Indicates whether output should be wrapped in an envelope containing response metadata. Defaults to no-envelope. */
public final boolean isEnvelope() {
return getOrDefault(RecordWriterStyleElementGroup.ENVELOPE)==RecordWriterStyleElement.envelope;
}

private final RecordWriterStyleElement getOrDefault(RecordWriterStyleElementGroup group) {
return styleElementsByGroup.getOrDefault(group, group.defaultStyle());
}
Expand All @@ -113,7 +118,8 @@ public static enum RecordWriterStyleElement {
border(RecordWriterStyleElementGroup.BORDER), no_border(RecordWriterStyleElementGroup.BORDER),
md_border(RecordWriterStyleElementGroup.BORDER),
wrap(RecordWriterStyleElementGroup.WRAP), no_wrap(RecordWriterStyleElementGroup.WRAP),
fast_output(RecordWriterStyleElementGroup.FAST_OUTPUT), no_fast_output(RecordWriterStyleElementGroup.FAST_OUTPUT)
fast_output(RecordWriterStyleElementGroup.FAST_OUTPUT), no_fast_output(RecordWriterStyleElementGroup.FAST_OUTPUT),
envelope(RecordWriterStyleElementGroup.ENVELOPE), no_envelope(RecordWriterStyleElementGroup.ENVELOPE)
;

@Getter private final RecordWriterStyleElementGroup group;
Expand All @@ -135,7 +141,8 @@ public static enum RecordWriterStyleElementGroup {
SINGULAR("array"),
BORDER("no-border"),
WRAP("wrap"),
FAST_OUTPUT("fast-output");
FAST_OUTPUT("fast-output"),
ENVELOPE("no-envelope");

private final String defaultStyleElementName;

Expand Down
Loading
Loading