fortify / fcli

fcli is a command-line utility for interacting with various Fortify products
https://fortify.github.io/fcli/
Other
27 stars 16 forks source link

Refactor output framework #537

Open rsenden opened 1 month ago

rsenden commented 1 month ago

With the current fcli output framework, we do a lot of dynamic interface-based lookups, i.e., AbstractOutputCommand checks whether the current command is an instance of IBaseRequestSupplier or IJsonNodeSupplier, from other places we check whether the command or product helper implements IRecordTransformer, IInputTransformer or IActionCommandResultSupplier, and we look up IProductHelper and INextPageUrlProducer instances from various places.

Initially this seemed like a good idea as it allows for simple command implementations, but as we kept adding more features, this became relatively complex and causes some limitations:

A good example of the latter is the fcli ssc issue list command. The SSC API only returns folderId and folderGuid, not folder name, making it difficult for users to query issues by folder or otherwise determining what folder an issue is in. Ideally, we'd use a record transformer to add a folderName property, but to do so, we'd need to have the filter set data. With the current architecture, we'd either need to store the filter set descriptor in an instance variable (which is a code smell as command instances may be reused, similar to how you shouldn't use instance variables in Java servlets to store request-specific data), or have the transformRecord() method load the filter set descriptor again and again (for every individual record) on each invocation (which would obviously be very bad for performance).

Essentially, the output and actions frameworks just needs to process a set of records, and shouldn't need to worry about next page URL producers, record transformers, ... So, possibly it would be better to have command implementations return something that simply produces such a set of records, for example represented as a dynamic Java (ordered) Stream (not sure how easy it is to produce such a stream from an HTTP response and attach functionality like loading multiple pages or doing transformations) or more traditionally, have interfaces like the following:

Taking the latter approach as an example, both the output and actions framework would provide one or more IRecordConsumer implementations for either writing or collecting records, returning Break.TRUE if necessary to tell the IRecordProducer to stop providing any further records (for reference, the Stream equivalent for optional breaking would be something like Stream::takeWhile, but we'd need an ordered stream for this to work properly).

Command implementations would implement the IRecordProducerSupplier interface and still (indirectly) extend for example AbstractOutputCommand, which will connect the command-provided IRecordProducer and output framework provided IRecordConsumer to produce command output. The actions framework can simply call getRecordProducer().processRecords(actionRecordCollector).

For convenience, product-specific classes like AbstractSSCOutputCommand could still implement getRecordProducer() by getting the UnirestInstance and then calling the abstract getRecordProducer(UnirestInstance) method to allow command implementations to easily access UnirestInstance, or command implementations could simply get the UnirestInstance from the superclass or explicit SSCUnirestInstanceSupplierMixin field; the latter may be better to avoid the need for multiple superclasses like AbstractSCSastControllerOutputCommand and AbstractSCSastSSCOutputCommand in modules that may access multiple systems.

With all of the above, the IRecordProducer would be responsible for things like paging and input & record transformations (potentially including adding a result column like currently done by IActionCommandResultSupplier). Not sure how we should handle current ISingularSupplier as it doesn't fit any of the interface methods listed above; we could:

Of course, we need to provide some infrastructure for easily building IRecordProducer instances, both for handling generic aspects like record transformations, and product-specific aspects like paging, for example to allow command implementations to do something like the below:

public IRecordProducer getRecordProducer() {
    var unirest = sscUnirestMixin.getUnirestInstance();
    var appVersionDescriptor = parentResolver.getAppVersion(unirest);
    return SSCRecordProducer.builder()
      .unirest(unirest)                     // or: .cmd(this), to allow SSCRecordProducer to invoke cmd.getUnirestInstanceSupplier().getUnirestInstance()
      .baseRequest(unirest.get(...)) // or: .jsonNode(SomeHelper.getXyzDescriptor().asJsonNode())
      .recordTransformer(record->this.transformRecord(unirest, appVersionDescriptor, record)
     .build();
}

This sample code should mostly work, but only because we never close any UnirestInstance until fcli termination (through GenericUnirestFactory.shutdown(), which is (maybe) something that we'd like to get rid of:

If (because of the second point) we ever change they way we manage UnirestInstance instances, we need to invent some way to create that instance when command execution starts and close it once command execution finishes, and make that instance (easily) available everywhere where it may be needed, like all of the methods mentioned above. Some potential ideas (that require further research):