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
@@ -1,20 +1,25 @@
package org.itsallcode.openfasttrace.api;

import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;

import org.itsallcode.openfasttrace.api.core.ItemStatus;

/**
* Settings for import filtering
*/
public final class FilterSettings
{
private final Set<String> artifactTypes;
private final Set<ItemStatus> wantedStatuses;
private final Set<String> tags;
private final boolean withoutTags;

private FilterSettings(final Builder builder)
{
this.artifactTypes = builder.artifactTypes;
this.wantedStatuses = builder.wantedStatuses;
this.tags = builder.tags;
this.withoutTags = builder.withoutTags;
}
Expand All @@ -29,6 +34,16 @@ public Set<String> getArtifactTypes()
return Set.copyOf(this.artifactTypes);
}

/**
* Get the statuses the filter must match.
*
* @return statuses that must be matched
*/
public Set<ItemStatus> getWantedStatuses()
{
return Set.copyOf(this.wantedStatuses);
}

/**
* Get the tags the filter must match.
*
Expand Down Expand Up @@ -60,6 +75,16 @@ public boolean isArtifactTypeCriteriaSet()
return this.artifactTypes != null && !this.artifactTypes.isEmpty();
}

/**
* Check if the status filter is set.
*
* @return {@code true} if the status filter is set
*/
public boolean isStatusCriteriaSet()
{
return this.wantedStatuses != null && !this.wantedStatuses.isEmpty();
Comment thread
redcatbear marked this conversation as resolved.
}

/**
* Check if the tag filter is set.
*
Expand All @@ -77,13 +102,13 @@ public boolean isTagCriteriaSet()
*/
public boolean isAnyCriteriaSet()
{
return isArtifactTypeCriteriaSet() || isTagCriteriaSet();
return isArtifactTypeCriteriaSet() || isStatusCriteriaSet() || isTagCriteriaSet();
}

@Override
public int hashCode()
{
return Objects.hash(this.artifactTypes, this.tags, this.withoutTags);
return Objects.hash(this.artifactTypes, this.wantedStatuses, this.tags, this.withoutTags);
}

@Override
Expand All @@ -92,7 +117,7 @@ public boolean equals(final Object other) {
return false;
}
return withoutTags == that.withoutTags && Objects.equals(artifactTypes, that.artifactTypes)
&& Objects.equals(tags, that.tags);
&& Objects.equals(wantedStatuses, that.wantedStatuses) && Objects.equals(tags, that.tags);
}

/**
Expand Down Expand Up @@ -122,6 +147,7 @@ public static Builder builder()
public static final class Builder
{
private Set<String> artifactTypes = Set.of();
private Set<ItemStatus> wantedStatuses = EnumSet.noneOf(ItemStatus.class);
private Set<String> tags = Set.of();
private boolean withoutTags = true;

Expand All @@ -143,6 +169,19 @@ public Builder artifactTypes(final Set<String> artifactTypes)
return this;
}

/**
* Set the list of statuses that the filter matches.
*
* @param statuses
* statuses that must be matched
* @return <code>this</code> for fluent programming
*/
public Builder wantedStatuses(final Set<ItemStatus> statuses)
{
this.wantedStatuses = Set.copyOf(statuses);
return this;
}

/**
* Set the list of tags that the filter matches.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,9 +200,17 @@ private SpecificationItem createNewSpecificationItem()
private boolean isAccepted(final SpecificationItem item)
{
return isAcceptedArtifactType(item.getArtifactType())
&& isAcceptedStatus(item.getStatus())
&& matchesTagsCriteria(item.getTags());
}

// [impl->dsn~filtering-by-item-status-during-import~1]
private boolean isAcceptedStatus(final ItemStatus status)
{
return !this.filterSettings.isStatusCriteriaSet()
|| this.filterSettings.getWantedStatuses().contains(status);
}

// [impl->dsn~filtering-by-tags-during-import~1]
// [impl->dsn~filtering-by-tags-or-no-tags-during-import~1]
private boolean matchesTagsCriteria(final List<String> tags)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import java.util.HashSet;

import org.itsallcode.openfasttrace.api.FilterSettings;
import org.itsallcode.openfasttrace.api.core.ItemStatus;
import org.junit.jupiter.api.Test;

import nl.jqno.equalsverifier.EqualsVerifier;
Expand All @@ -31,6 +32,17 @@ void testBuilder()
assertFilterSet(filterSettings, true);
}

@Test
void testBuilderWithStatuses()
{
final ItemStatus[] expectedStatuses = { ItemStatus.APPROVED, ItemStatus.DRAFT };
final FilterSettings filterSettings = FilterSettings.builder() //
.wantedStatuses(new HashSet<>(Arrays.asList(expectedStatuses))) //
.build();
assertThat(filterSettings.getWantedStatuses(), containsInAnyOrder(expectedStatuses));
assertFilterSet(filterSettings, true);
Comment thread
redcatbear marked this conversation as resolved.
}

private void assertFilterSet(final FilterSettings filterSettings, final boolean set)
{
assertThat(filterSettings.isAnyCriteriaSet(), equalTo(set));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,40 @@ void testDuplicateIdNotIgnored()
assertThat(builder.getItemCount(), equalTo(2));
}

// [utest->dsn~filtering-by-item-status-during-import~1]
@Test
void testFilterSpecificationItemsByStatus()
{
final Set<ItemStatus> wantedStatuses = new HashSet<>();
wantedStatuses.add(ItemStatus.DRAFT);
final FilterSettings filterSettings = FilterSettings.builder() //
.wantedStatuses(wantedStatuses) //
.build();
final SpecificationListBuilder builder = SpecificationListBuilder
.createWithFilter(filterSettings);
addItemWithStatus(builder, "in-A", ItemStatus.DRAFT);
addItemWithStatus(builder, "out-B", ItemStatus.APPROVED);
addItemWithStatus(builder, "out-C", ItemStatus.PROPOSED);
addItemWithStatus(builder, "out-D", ItemStatus.REJECTED);
addItemWithStatus(builder, "out-E", null); // becomes APPROVED by default
final List<SpecificationItem> items = builder.build();
assertThat(items.stream().map(SpecificationItem::getName).toList(),
containsInAnyOrder("in-A"));
}

private void addItemWithStatus(final SpecificationListBuilder builder, final String name,
final ItemStatus status)
{
builder.beginSpecificationItem();
final SpecificationItemId id = SpecificationItemId.createId("dsn", name, 1);
builder.setId(id);
if (status != null)
{
builder.setStatus(status);
}
builder.endSpecificationItem();
}

// [utest->dsn~filtering-by-tags-during-import~1]
@Test
void testFilterSpecificationItemsByTags()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
import java.nio.file.Paths;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.itsallcode.openfasttrace.api.ColorScheme;
import org.itsallcode.openfasttrace.api.DetailsSectionDisplay;
import org.itsallcode.openfasttrace.api.cli.DirectoryService;
import org.itsallcode.openfasttrace.api.core.ItemStatus;
import org.itsallcode.openfasttrace.api.core.Newline;
import org.itsallcode.openfasttrace.api.report.ReportConstants;
import org.itsallcode.openfasttrace.api.report.ReportVerbosity;
Expand All @@ -33,6 +35,7 @@ public class CliArguments
private String outputFormat;
private ReportVerbosity reportVerbosity;
private Set<String> wantedArtifactTypes = Collections.emptySet();
private Set<ItemStatus> wantedStatuses = Collections.emptySet();
private Set<String> wantedTags = Collections.emptySet();

// [impl->dsn~reporting.plain-text.specification-item-origin~1]]
Expand Down Expand Up @@ -329,6 +332,45 @@ public void setA(final String artifactTypes)
setWantedArtifactTypes(artifactTypes);
}

/**
* Get a list of statuses to be applied as a filter during import
*
* @return set of wanted statuses
*/
public Set<ItemStatus> getWantedStatuses()
{
return Collections.unmodifiableSet(this.wantedStatuses);
}

/**
* Set a list of statuses to be applied as a filter during import
*
* @param statuses
* list of wanted statuses
*/
public void setWantedStatuses(final String statuses)
{
this.wantedStatuses = createStatusSetFromCommaSeparatedString(statuses);
}

private static Set<ItemStatus> createStatusSetFromCommaSeparatedString(final String commaSeparatedString)
{
return COMMA_SEPARATED_PATTERN.splitAsStream(commaSeparatedString)
.map(ItemStatus::parseString)
.collect(Collectors.toSet());
}

/**
* Set a list of statuses to be applied as a filter during import
*
* @param statuses
* list of wanted statuses
*/
public void setW(final String statuses)
{
setWantedStatuses(statuses);
}

/**
* Get a list of tags to be applied as a filter during import
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,19 @@ private FilterSettings createFilterSettingsFromArguments()
final FilterSettings.Builder builder = FilterSettings.builder();
setAttributeTypeFilter(builder);
setTagFilter(builder);
setStatusFilter(builder);
return builder.build();
}

private void setStatusFilter(final FilterSettings.Builder builder)
{
if (this.arguments.getWantedStatuses() != null
&& !this.arguments.getWantedStatuses().isEmpty())
{
builder.wantedStatuses(this.arguments.getWantedStatuses());
}
}

private void setAttributeTypeFilter(final FilterSettings.Builder builder)
{
if (this.arguments.getWantedArtifactTypes() != null
Expand Down
2 changes: 2 additions & 0 deletions core/src/main/resources/usage.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ Common options:
Note that this option is ignored when -f is also
set.
-f, --output-file path The output file. Defaults to STDOUT.
-w, --wanted-statuses Import only specification items that have a
status contained in the comma-separated list
-n, --newline format Newline format. One of "unix", "windows", "oldmac"
-t, --wanted-tags Import only specification items that have at
least one tag contained in the comma-separated
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import org.itsallcode.openfasttrace.api.ColorScheme;
import org.itsallcode.openfasttrace.api.DetailsSectionDisplay;
import org.itsallcode.openfasttrace.api.core.ItemStatus;
import org.itsallcode.openfasttrace.api.core.Newline;
import org.itsallcode.openfasttrace.api.report.ReportConstants;
import org.itsallcode.openfasttrace.api.report.ReportVerbosity;
Expand Down Expand Up @@ -166,6 +167,33 @@ void testSetA()
containsInAnyOrder("impl", "utest"));
}

// [utest->dsn~filtering-by-item-status-during-import~1]
@Test
void testWantedStatusesEmptyByDefault()
{
assertThat(BEFORE_SETTER, this.arguments.getWantedStatuses(), emptyIterable());
}

// [utest->dsn~filtering-by-item-status-during-import~1]
@Test
void testSetWantedStatuses()
{
final String value = "approved,proposed";
this.arguments.setWantedStatuses(value);
assertThat(AFTER_SETTER, this.arguments.getWantedStatuses(),
containsInAnyOrder(ItemStatus.APPROVED, ItemStatus.PROPOSED));
}

// [utest->dsn~filtering-by-item-status-during-import~1]
@Test
void testSetW()
{
final String value = "draft, proposed";
this.arguments.setW(value);
assertThat(AFTER_SETTER, this.arguments.getWantedStatuses(),
containsInAnyOrder(ItemStatus.DRAFT, ItemStatus.PROPOSED));
}

// [utest->dsn~filtering-by-tags-during-import~1]
@Test
void testWantedTagsEmptyByDefault()
Expand Down
15 changes: 11 additions & 4 deletions doc/changes/changes_4.6.0.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
# OpenFastTrace 4.6.0, released 2026-07-??

Code name: ??

We also updated test and build dependencies to fix vulnerabilities. Runtime code is not affected, so no update is required.
Code name: Status Filter at Import

## Summary

We moved some GitHub action permissions from workflow-level to job-level.
We added a new feature to filter specification items by status at import time via `-w` or `--wanted-statuses` CLI parameters.
This is helpful when your project uses specification documents for planning future requirements (marked by the status "draft" or "porposed").
Comment on lines +7 to +8

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A typical use case is to exclude only draft items. If I understand correctly, this would require specifying all other status in this option?

Another remark: We often don't use the status field for items. Only when we add items we mark them as draft. Would it be possible to allow including items without status?


We also updated test and build dependencies to fix vulnerabilities. Runtime code is not affected, so no update is required.

We moved some GitHub action permissions from workflow-level to job-level. Sonar findings that accumulated with Sonar introducing new code rules were reduced a lot. Since OFT is used in safety-critical projects, code quality is crucial to us.

## New Features

* #519: Added support for filtering by specification item status.

## Security

* #556: Updated Junit, PlantUML, Jacoco Maven plugin and Central publishing Plugin dependencies to fix vulnerabilities
Expand Down
11 changes: 11 additions & 0 deletions doc/spec/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,17 @@ Covers:

Needs: impl, utest, itest

#### Filtering by Item Status During Import
`dsn~filtering-by-item-status-during-import~1`

The [specification list builder](#specification-list-builder) can be configured to import a specification item only if its status matches at least one of the configured statuses.

Covers:

* `req~include-only-item-statuses~1`

Needs: impl, utest, itest

#### Filtering by Tags During Import
`dsn~filtering-by-tags-during-import~1`

Expand Down
11 changes: 11 additions & 0 deletions doc/spec/system_requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,17 @@ Covers:

Needs: dsn

#### Include Only Item Statuses
`req~include-only-item-statuses~1`

OFT gives users the option to include only specification items with a configurable set of statuses during processing.

Covers:

* [feat~requirement-tracing~1](#requirement-tracing)

Needs: dsn

#### Include Items Where at Least One Tag Matches
`req~include-items-where-at-least-on-tag-matches~1`

Expand Down
Loading
Loading