This is an automated email from the ASF dual-hosted git repository.
pgyori pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push:
new 0a5be35357 NIFI-13030 Adding endpoint for comparing versions of
registered flows
0a5be35357 is described below
commit 0a5be3535711378caceb3fd529bf3ce86986da99
Author: Bence Simon <[email protected]>
AuthorDate: Fri Apr 19 11:16:26 2024 +0200
NIFI-13030 Adding endpoint for comparing versions of registered flows
This closes #8670
Signed-off-by: Peter Gyori <[email protected]>
---
.../nifi/registry/flow/FlowVersionLocation.java | 17 +++
.../org/apache/nifi/web/api/dto/DifferenceDTO.java | 14 ++
.../org/apache/nifi/web/NiFiServiceFacade.java | 11 ++
.../apache/nifi/web/StandardNiFiServiceFacade.java | 36 +++++
.../java/org/apache/nifi/web/api/FlowResource.java | 117 ++++++++++++++++
.../apache/nifi/web/util/ClosedOpenInterval.java | 82 +++++++++++
.../java/org/apache/nifi/web/util/Interval.java | 63 +++++++++
.../org/apache/nifi/web/util/IntervalFactory.java} | 35 ++---
.../org/apache/nifi/web/util/PaginationHelper.java | 99 +++++++++++++
.../org/apache/nifi/web/api/TestFlowResource.java | 156 +++++++++++++++++++++
.../nifi/web/util/ClosedOpenIntervalTest.java | 98 +++++++++++++
.../apache/nifi/web/util/PaginationHelperTest.java | 79 +++++++++++
12 files changed, 781 insertions(+), 26 deletions(-)
diff --git
a/nifi-api/src/main/java/org/apache/nifi/registry/flow/FlowVersionLocation.java
b/nifi-api/src/main/java/org/apache/nifi/registry/flow/FlowVersionLocation.java
index e615b6a45d..f93ef63649 100644
---
a/nifi-api/src/main/java/org/apache/nifi/registry/flow/FlowVersionLocation.java
+++
b/nifi-api/src/main/java/org/apache/nifi/registry/flow/FlowVersionLocation.java
@@ -19,6 +19,8 @@
package org.apache.nifi.registry.flow;
+import java.util.Objects;
+
/**
* Information for locating a flow version in a flow registry.
*/
@@ -43,4 +45,19 @@ public class FlowVersionLocation extends FlowLocation {
this.version = version;
}
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ final FlowVersionLocation that = (FlowVersionLocation) o;
+ return Objects.equals(getBranch(), that.getBranch())
+ && Objects.equals(getBucketId(), that.getBucketId())
+ && Objects.equals(getFlowId(), that.getFlowId())
+ && Objects.equals(version, that.version);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getBranch(), getBucketId(), getFlowId(), version);
+ }
}
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/DifferenceDTO.java
b/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/DifferenceDTO.java
index 0b1dd2a087..913084db52 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/DifferenceDTO.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/DifferenceDTO.java
@@ -21,6 +21,8 @@ import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.xml.bind.annotation.XmlType;
+import java.util.Objects;
+
@XmlType(name = "difference")
public class DifferenceDTO {
private String differenceType;
@@ -44,4 +46,16 @@ public class DifferenceDTO {
this.difference = difference;
}
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ final DifferenceDTO that = (DifferenceDTO) o;
+ return Objects.equals(differenceType, that.differenceType) &&
Objects.equals(difference, that.difference);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(differenceType, difference);
+ }
}
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
index b372d5cc83..ee6ea5982f 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
@@ -37,6 +37,7 @@ import org.apache.nifi.parameter.ParameterContext;
import org.apache.nifi.parameter.ParameterGroupConfiguration;
import org.apache.nifi.registry.flow.FlowLocation;
import org.apache.nifi.registry.flow.FlowSnapshotContainer;
+import org.apache.nifi.registry.flow.FlowVersionLocation;
import org.apache.nifi.registry.flow.RegisterAction;
import org.apache.nifi.registry.flow.RegisteredFlow;
import org.apache.nifi.registry.flow.RegisteredFlowSnapshot;
@@ -1501,6 +1502,16 @@ public interface NiFiServiceFacade {
*/
RegisteredFlow deleteVersionedFlow(String registryId, String branch,
String bucketId, String flowId);
+ /**
+ * Returns the differences of version B from version A.
+ *
+ * @param registryId the ID of the registry
+ * @param versionLocationA Location of the baseline snapshot of the
comparison
+ * @param versionLocationB location of the compared snapshot
+ * @return the differences between the snapshots
+ */
+ FlowComparisonEntity getVersionDifference(String registryId,
FlowVersionLocation versionLocationA, FlowVersionLocation versionLocationB);
+
/**
* Adds the given snapshot to the already existing Versioned Flow, which
resides in the given Flow Registry with the given id
*
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
index dfa6f0df90..ce7c15ba70 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
@@ -5258,6 +5258,42 @@ public class StandardNiFiServiceFacade implements
NiFiServiceFacade {
}
}
+ @Override
+ public FlowComparisonEntity getVersionDifference(final String registryId,
FlowVersionLocation versionLocationA, FlowVersionLocation versionLocationB) {
+ final FlowComparisonEntity result = new FlowComparisonEntity();
+
+ if (versionLocationA.equals(versionLocationB)) {
+ // If both versions are the same, there is no need for comparison.
Comparing them should have the same result but with the cost of some calls to
the registry.
+ // Note: because of this optimization we return an empty non-error
response in case of non-existing registry, bucket, flow or version if the
versions are the same.
+ result.setComponentDifferences(Collections.emptySet());
+ return result;
+ }
+
+ final FlowSnapshotContainer snapshotA = this.getVersionedFlowSnapshot(
+ registryId, versionLocationA.getBranch(),
versionLocationA.getBucketId(), versionLocationA.getFlowId(),
versionLocationA.getVersion(), true);
+ final FlowSnapshotContainer snapshotB = this.getVersionedFlowSnapshot(
+ registryId, versionLocationB.getBranch(),
versionLocationB.getBucketId(), versionLocationB.getFlowId(),
versionLocationB.getVersion(), true);
+
+ final VersionedProcessGroup flowContentsA =
snapshotA.getFlowSnapshot().getFlowContents();
+ final VersionedProcessGroup flowContentsB =
snapshotB.getFlowSnapshot().getFlowContents();
+
+ final FlowComparator flowComparator = new StandardFlowComparator(
+ new StandardComparableDataFlow("Flow A", flowContentsA),
+ new StandardComparableDataFlow("Flow B", flowContentsB),
+ Collections.emptySet(), // Replacement of an external
ControllerService is recognized as property change
+ new ConciseEvolvingDifferenceDescriptor(),
+ Function.identity(),
+ VersionedComponent::getIdentifier,
+ FlowComparatorVersionedStrategy.DEEP
+ );
+
+ final FlowComparison flowComparison = flowComparator.compare();
+ final Set<ComponentDifferenceDTO> differenceDtos =
dtoFactory.createComponentDifferenceDtosForLocalModifications(flowComparison,
flowContentsA, controllerFacade.getFlowManager());
+ result.setComponentDifferences(differenceDtos);
+
+ return result;
+ }
+
@Override
public boolean isAnyProcessGroupUnderVersionControl(final String groupId) {
return
isProcessGroupUnderVersionControl(processGroupDAO.getProcessGroup(groupId));
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java
index c62147a368..ed71ead166 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java
@@ -49,6 +49,7 @@ import org.apache.nifi.flow.VersionedReportingTaskSnapshot;
import org.apache.nifi.groups.ProcessGroup;
import org.apache.nifi.nar.NarClassLoadersHolder;
import org.apache.nifi.registry.client.NiFiRegistryException;
+import org.apache.nifi.registry.flow.FlowVersionLocation;
import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.web.IllegalClusterResourceRequestException;
import org.apache.nifi.web.NiFiServiceFacade;
@@ -60,6 +61,8 @@ import org.apache.nifi.web.api.dto.BulletinBoardDTO;
import org.apache.nifi.web.api.dto.BulletinQueryDTO;
import org.apache.nifi.web.api.dto.ClusterDTO;
import org.apache.nifi.web.api.dto.ClusterSummaryDTO;
+import org.apache.nifi.web.api.dto.ComponentDifferenceDTO;
+import org.apache.nifi.web.api.dto.DifferenceDTO;
import org.apache.nifi.web.api.dto.NodeDTO;
import org.apache.nifi.web.api.dto.ProcessGroupDTO;
import org.apache.nifi.web.api.dto.RevisionDTO;
@@ -89,6 +92,7 @@ import org.apache.nifi.web.api.entity.CurrentUserEntity;
import org.apache.nifi.web.api.entity.FlowAnalysisResultEntity;
import org.apache.nifi.web.api.entity.FlowAnalysisRuleTypesEntity;
import org.apache.nifi.web.api.entity.FlowBreadcrumbEntity;
+import org.apache.nifi.web.api.entity.FlowComparisonEntity;
import org.apache.nifi.web.api.entity.FlowConfigurationEntity;
import org.apache.nifi.web.api.entity.FlowRegistryBranchEntity;
import org.apache.nifi.web.api.entity.FlowRegistryBranchesEntity;
@@ -145,6 +149,7 @@ import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.StreamingOutput;
+import org.apache.nifi.web.util.PaginationHelper;
import java.text.Collator;
import java.time.OffsetDateTime;
@@ -2057,6 +2062,100 @@ public class FlowResource extends ApplicationResource {
return generateOkResponse(flowDetails).build();
}
+ @GET
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.APPLICATION_JSON)
+
@Path("registries/{registry-id}/branches/{branch-id-a}/buckets/{bucket-id-a}/flows/{flow-id-a}/{version-a}/diff/branches/{branch-id-b}/buckets/{bucket-id-b}/flows/{flow-id-b}/{version-b}")
+ @Operation(
+ summary = "Gets the differences between two versions of the same
versioned flow, the basis of the comparison will be the first version",
+ responses = @ApiResponse(content = @Content(schema =
@Schema(implementation = FlowComparisonEntity.class))),
+ security = {
+ @SecurityRequirement(name = "Read - /flow")
+ }
+ )
+ @ApiResponses(
+ value = {
+ @ApiResponse(responseCode = "400", description = "NiFi was
unable to complete the request because it was invalid. The request should not
be retried without modification."),
+ @ApiResponse(responseCode = "401", description = "Client
could not be authenticated."),
+ @ApiResponse(responseCode = "403", description = "Client
is not authorized to make this request."),
+ @ApiResponse(responseCode = "404", description = "The
specified resource could not be found."),
+ @ApiResponse(responseCode = "409", description = "The
request was valid but NiFi was not in the appropriate state to process it.")
+ }
+ )
+ public Response getVersionDifferences(
+ @Parameter(
+ description = "The registry client id.",
+ required = true
+ )
+ @PathParam("registry-id") String registryId,
+
+ @Parameter(
+ description = "The branch id for the base version.",
+ required = true
+ )
+ @PathParam("branch-id-a") String branchIdA,
+
+ @Parameter(
+ description = "The bucket id for the base version.",
+ required = true
+ )
+ @PathParam("bucket-id-a") String bucketIdA,
+
+ @Parameter(
+ description = "The flow id for the base version.",
+ required = true
+ )
+ @PathParam("flow-id-a") String flowIdA,
+
+ @Parameter(
+ description = "The base version.",
+ required = true
+ )
+ @PathParam("version-a") String versionA,
+
+ @Parameter(
+ description = "The branch id for the compared version.",
+ required = true
+ )
+ @PathParam("branch-id-b") String branchIdB,
+
+ @Parameter(
+ description = "The bucket id for the compared version.",
+ required = true
+ )
+ @PathParam("bucket-id-b") String bucketIdB,
+
+ @Parameter(
+ description = "The flow id for the compared version.",
+ required = true
+ )
+ @PathParam("flow-id-b") String flowIdB,
+
+ @Parameter(
+ description = "The compared version.",
+ required = true
+ )
+ @PathParam("version-b") String versionB,
+ @QueryParam("offset")
+ @Parameter(description = "Must be a non-negative number. Specifies
the starting point of the listing. 0 means start from the beginning.")
+ @DefaultValue("0")
+ int offset,
+ @QueryParam("limit")
+ @Parameter(description = "Limits the number of differences listed.
This might lead to partial result. 0 means no limitation is applied.")
+ @DefaultValue("1000")
+ int limit
+ ) {
+ authorizeFlow();
+ FlowVersionLocation baseVersionLocation = new
FlowVersionLocation(branchIdA, bucketIdA, flowIdA, versionA);
+ FlowVersionLocation comparedVersionLocation = new
FlowVersionLocation(branchIdB, bucketIdB, flowIdB, versionB);
+ final FlowComparisonEntity versionDifference =
serviceFacade.getVersionDifference(registryId, baseVersionLocation,
comparedVersionLocation);
+ // Note: with the current implementation, this is deterministic.
However, the internal data structure used in comparison is set, thus
+ // later changes might cause discrepancies. Practical use of the
endpoint usually remains within one "page" though.
+ return generateOkResponse(limitDifferences(versionDifference, offset,
limit))
+ .type(MediaType.APPLICATION_JSON_TYPE)
+ .build();
+ }
+
@GET
@Consumes(MediaType.WILDCARD)
@Produces(MediaType.APPLICATION_JSON)
@@ -2104,6 +2203,24 @@ public class FlowResource extends ApplicationResource {
return
generateOkResponse(versionedFlowSnapshotMetadataSetEntity).build();
}
+ private static FlowComparisonEntity limitDifferences(final
FlowComparisonEntity original, final int offset, final int limit) {
+ final List<ComponentDifferenceDTO> limited =
PaginationHelper.paginateByContainedItems(
+ original.getComponentDifferences(), offset, limit,
ComponentDifferenceDTO::getDifferences, FlowResource::limitDifferences);
+ final FlowComparisonEntity result = new FlowComparisonEntity();
+ result.setComponentDifferences(new HashSet<>(limited));
+ return result;
+ }
+
+ private static ComponentDifferenceDTO limitDifferences(final
ComponentDifferenceDTO original, final List<DifferenceDTO> partial) {
+ final ComponentDifferenceDTO result = new ComponentDifferenceDTO();
+ result.setComponentType(original.getComponentType());
+ result.setComponentId(original.getComponentId());
+ result.setComponentName(original.getComponentName());
+ result.setProcessGroupId(original.getProcessGroupId());
+ result.setDifferences(partial);
+ return result;
+ }
+
// --------------
// bulletin board
// --------------
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/ClosedOpenInterval.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/ClosedOpenInterval.java
new file mode 100644
index 0000000000..224aea94a6
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/ClosedOpenInterval.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.util;
+
+/**
+ * This implementation includes the lower boundary but does not include the
higher boundary.
+ */
+final class ClosedOpenInterval implements Interval {
+ private final int lowerBoundary;
+ private final int higherBoundary;
+
+ /**
+ * @param lowerBoundary Inclusive index of lower boundary
+ * @param higherBoundary Exclusive index of higher boundary. In case of 0,
the higher boundary is unspecified and the interval is open.
+ */
+ ClosedOpenInterval(final int lowerBoundary, final int higherBoundary) {
+ if (lowerBoundary < 0) {
+ throw new IllegalArgumentException("Lower boundary cannot be
negative");
+ }
+
+ if (higherBoundary < 0) {
+ throw new IllegalArgumentException("Higher boundary cannot be
negative");
+ }
+
+ if (higherBoundary <= lowerBoundary && higherBoundary != 0) {
+ throw new IllegalArgumentException(
+ "Higher boundary cannot be lower than or equal to lower
boundary except when unspecified. Higher boundary is considered unspecified
when the value is set to 0"
+ );
+ }
+
+ this.lowerBoundary = lowerBoundary;
+ this.higherBoundary = higherBoundary;
+ }
+
+ @Override
+ public RelativePosition getRelativePositionOf(final int
otherIntervalLowerBoundary, final int otherIntervalHigherBoundary) {
+ if (otherIntervalLowerBoundary < 0) {
+ throw new IllegalArgumentException("Lower boundary cannot be
negative");
+ }
+
+ if (otherIntervalHigherBoundary <= 0) {
+ // Note: as a design decision the implementation currently does
not support comparison with unspecified higher boundary
+ throw new IllegalArgumentException("Higher boundary must be
positive");
+ }
+
+ if (otherIntervalLowerBoundary >= otherIntervalHigherBoundary) {
+ throw new IllegalArgumentException("Higher boundary must be
greater than lower boundary");
+ }
+
+ if (otherIntervalHigherBoundary <= lowerBoundary) {
+ return RelativePosition.BEFORE;
+ } else if (otherIntervalLowerBoundary < lowerBoundary &&
otherIntervalHigherBoundary > higherBoundary && !this.isEndUnspecified()) {
+ return RelativePosition.EXCEEDS;
+ } else if (otherIntervalLowerBoundary < lowerBoundary) {
+ return RelativePosition.TAIL_INTERSECTS;
+ } else if (otherIntervalHigherBoundary <= higherBoundary ||
this.isEndUnspecified()) {
+ return RelativePosition.WITHIN;
+ } else if (otherIntervalLowerBoundary < higherBoundary) {
+ return RelativePosition.HEAD_INTERSECTS;
+ } else {
+ return RelativePosition.AFTER;
+ }
+ }
+
+ private boolean isEndUnspecified() {
+ return higherBoundary == 0;
+ }
+}
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/Interval.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/Interval.java
new file mode 100644
index 0000000000..59a0c15909
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/Interval.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.util;
+
+public interface Interval {
+
+ enum RelativePosition {
+ /**
+ * The compared interval ends before the actual, there is no
intersection.
+ */
+ BEFORE,
+
+ /**
+ * The compared interval exceeds the actual both at the low and high
ends.
+ */
+ EXCEEDS,
+
+ /**
+ * The compared interval's tail (but not the whole interval)
intersects the actual interval (part of it or the whole actual interval).
+ */
+ TAIL_INTERSECTS,
+
+ /**
+ * The compared interval is within the actual interval. It can match
with the actual or contained by that.
+ */
+ WITHIN,
+
+ /**
+ *The compared interval's head (but not the whole interval) intersects
the actual interval (part of it or the whole actual interval).
+ */
+ HEAD_INTERSECTS,
+
+ /**
+ * The compared interval starts after the actual, there is no
intersection.
+ */
+ AFTER,
+ }
+
+ /**
+ * Relative position of the "other" interval compared to this.
+ *
+ * @param otherIntervalLowerBoundary Lower boundary of the compared
interval.
+ * @param otherIntervalHigherBoundary Higher boundary of the compared
interval.
+ *
+ * @return Returns the relative position of the "other" interval compared
to this interval. For example: if the result
+ * is BEFORE, read it as: the other interval ends BEFORE the
actual (and there is no intersection between them).
+ */
+ RelativePosition getRelativePositionOf(final int
otherIntervalLowerBoundary, final int otherIntervalHigherBoundary);
+}
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/DifferenceDTO.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/IntervalFactory.java
similarity index 53%
copy from
nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/DifferenceDTO.java
copy to
nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/IntervalFactory.java
index 0b1dd2a087..9d26eae1ec 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/DifferenceDTO.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/IntervalFactory.java
@@ -14,34 +14,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package org.apache.nifi.web.util;
-package org.apache.nifi.web.api.dto;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-
-import jakarta.xml.bind.annotation.XmlType;
-
-@XmlType(name = "difference")
-public class DifferenceDTO {
- private String differenceType;
- private String difference;
-
- @Schema(description = "The type of difference")
- public String getDifferenceType() {
- return differenceType;
+public final class IntervalFactory {
+ private IntervalFactory() {
+ // Not to be instantiated
}
- public void setDifferenceType(String differenceType) {
- this.differenceType = differenceType;
+ /**
+ * @return Returns an interval instance with closed low and open high
boundary.
+ */
+ static Interval getClosedOpenInterval(final int lowerBoundary, final int
higherBoundary) {
+ return new ClosedOpenInterval(lowerBoundary, higherBoundary);
}
-
- @Schema(description = "Description of the difference")
- public String getDifference() {
- return difference;
- }
-
- public void setDifference(String difference) {
- this.difference = difference;
- }
-
}
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/PaginationHelper.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/PaginationHelper.java
new file mode 100644
index 0000000000..04c90cfbd1
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/PaginationHelper.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.util;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+public class PaginationHelper {
+ public static <T, E> List<T> paginateByContainedItems(
+ final Iterable<T> original,
+ final int offset,
+ final int limit,
+ final Function<T, List<E>> getContainedItems,
+ final BiFunction<T, List<E>, T> createPartialItem
+ ) {
+ Objects.requireNonNull(original);
+ Objects.requireNonNull(getContainedItems);
+ Objects.requireNonNull(createPartialItem);
+
+ if (offset < 0) {
+ throw new IllegalArgumentException("Offset cannot be negative");
+ }
+
+ if (limit < 0) {
+ throw new IllegalArgumentException("Limit cannot be negative");
+ }
+
+ final List<T> result = new LinkedList<>();
+ final int higherBoundary = limit == 0 ? 0 : offset + limit;
+ final Interval interval =
IntervalFactory.getClosedOpenInterval(offset, higherBoundary);
+ int pointer = 0;
+
+ if (offset == 0 && limit == 0) {
+ original.forEach(result::add);
+ return result;
+ }
+
+ for (final T candidate : original) {
+ final List<E> containedItems = getContainedItems.apply(candidate);
+ final ClosedOpenInterval.RelativePosition position =
interval.getRelativePositionOf(pointer, pointer + containedItems.size());
+
+ switch (position) {
+ case BEFORE: {
+ pointer += containedItems.size();
+ break;
+ }
+ case EXCEEDS: {
+ final int startingPoint = offset - pointer;
+ final List<E> partialItems =
containedItems.subList(startingPoint, limit + 1);
+ final T partial = createPartialItem.apply(candidate,
partialItems);
+ result.add(partial);
+ pointer += startingPoint + partialItems.size();
+ break;
+ }
+ case TAIL_INTERSECTS: {
+ final List<E> partialItems = containedItems.subList(offset
- pointer, containedItems.size());
+ final T partial = createPartialItem.apply(candidate,
partialItems);
+ result.add(partial);
+ pointer += containedItems.size();
+ break;
+ }
+ case WITHIN: {
+ result.add(candidate);
+ pointer += containedItems.size();
+ break;
+ }
+ case HEAD_INTERSECTS: {
+ final List<E> partialItems = containedItems.subList(0,
limit + offset - pointer);
+ final T partial = createPartialItem.apply(candidate,
partialItems);
+ result.add(partial);
+ pointer += partialItems.size();
+ break;
+ }
+ case AFTER:
+ default:
+ // Do nothing
+ }
+ }
+
+ return result;
+ }
+}
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/TestFlowResource.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/TestFlowResource.java
index 58fa0395e4..8635b709bf 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/TestFlowResource.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/TestFlowResource.java
@@ -33,8 +33,12 @@ import
org.apache.nifi.prometheus.util.ConnectionAnalyticsMetricsRegistry;
import org.apache.nifi.prometheus.util.JvmMetricsRegistry;
import org.apache.nifi.prometheus.util.NiFiMetricsRegistry;
import org.apache.nifi.prometheus.util.PrometheusMetricsUtil;
+import org.apache.nifi.registry.flow.FlowVersionLocation;
import org.apache.nifi.web.NiFiServiceFacade;
import org.apache.nifi.web.ResourceNotFoundException;
+import org.apache.nifi.web.api.dto.ComponentDifferenceDTO;
+import org.apache.nifi.web.api.dto.DifferenceDTO;
+import org.apache.nifi.web.api.entity.FlowComparisonEntity;
import org.apache.nifi.web.api.request.FlowMetricsProducer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -50,10 +54,13 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -62,6 +69,9 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anySet;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@@ -82,6 +92,13 @@ public class TestFlowResource {
private static final int COMPONENT_TYPE_VALUE_INDEX = 1;
private static final String CLUSTER_TYPE_LABEL = "cluster";
private static final String CLUSTER_LABEL_KEY = "instance";
+ private static final String SAMPLE_REGISTRY_ID =
"0e87642a-7720-4799-a3bd-04db74b86e85";
+ private static final String SAMPLE_BRANCH_ID_A =
"c302f541-976e-4c51-952d-345516444e3d";
+ private static final String SAMPLE_BUCKET_ID_A =
"23da421d-a8da-4fa3-939e-658d8f35b972";
+ private static final String SAMPLE_FLOW_ID_A =
"34e4c8c5-f61d-45a4-8035-2aa3641ae904";
+ private static final String SAMPLE_BRANCH_ID_B =
"fae2ef59-eb0d-4de6-ae31-342089fd229f";
+ private static final String SAMPLE_BUCKET_ID_B =
"42998285-d06c-41dd-a757-7a14ab9673f4";
+ private static final String SAMPLE_FLOW_ID_B =
"e6483662-9226-41c1-adec-10357af97ce2";
@InjectMocks
private FlowResource resource = new FlowResource();
@@ -282,6 +299,145 @@ public class TestFlowResource {
assertEquals(2L, result.get(SAMPLE_LABEL_VALUES_ROOT_PROCESS_GROUP));
}
+ @Test
+ public void testGetVersionDifferencesWithoutLimitations() {
+ setUpGetVersionDifference();
+
+ final Response response = resource.getVersionDifferences(
+ SAMPLE_REGISTRY_ID, SAMPLE_BRANCH_ID_A, SAMPLE_BUCKET_ID_A,
SAMPLE_FLOW_ID_A, "1", SAMPLE_BRANCH_ID_B, SAMPLE_BUCKET_ID_B,
SAMPLE_FLOW_ID_B, "2", 0, 0);
+ assertNotNull(response);
+ assertEquals(MediaType.valueOf(MediaType.APPLICATION_JSON),
response.getMediaType());
+
assertTrue(FlowComparisonEntity.class.isInstance(response.getEntity()));
+
+ final FlowComparisonEntity entity = (FlowComparisonEntity)
response.getEntity();
+ final List<DifferenceDTO> differences =
entity.getComponentDifferences().stream().map(ComponentDifferenceDTO::getDifferences).flatMap(Collection::stream).collect(Collectors.toList());
+ assertEquals(5, differences.size());
+ }
+
+ @Test
+ public void testGetVersionDifferencesFromBeginningWithPartialResults() {
+ setUpGetVersionDifference();
+
+ final Response response = resource.getVersionDifferences(
+ SAMPLE_REGISTRY_ID, SAMPLE_BRANCH_ID_A, SAMPLE_BUCKET_ID_A,
SAMPLE_FLOW_ID_A, "1", SAMPLE_BRANCH_ID_B, SAMPLE_BUCKET_ID_B,
SAMPLE_FLOW_ID_B, "2", 0, 2
+ );
+
+ assertNotNull(response);
+ assertEquals(MediaType.valueOf(MediaType.APPLICATION_JSON),
response.getMediaType());
+
assertTrue(FlowComparisonEntity.class.isInstance(response.getEntity()));
+
+ final FlowComparisonEntity entity = (FlowComparisonEntity)
response.getEntity();
+ final List<DifferenceDTO> differences =
entity.getComponentDifferences().stream().map(ComponentDifferenceDTO::getDifferences).flatMap(Collection::stream).collect(Collectors.toList());
+ assertEquals(2, differences.size());
+ assertEquals(createDifference("Component Added", "Connection was
added"), differences.get(0));
+ assertEquals(createDifference("Property Value Changed", "From '0B' to
'1KB'"), differences.get(1));
+ }
+
+ @Test
+ public void
testGetVersionDifferencesFromBeginningExtendedWithPartialResults() {
+ setUpGetVersionDifference();
+
+ final Response response = resource.getVersionDifferences(
+ SAMPLE_REGISTRY_ID, SAMPLE_BRANCH_ID_A, SAMPLE_BUCKET_ID_A,
SAMPLE_FLOW_ID_A, "1", SAMPLE_BRANCH_ID_B, SAMPLE_BUCKET_ID_B,
SAMPLE_FLOW_ID_B, "2", 0, 3
+ );
+
+ assertNotNull(response);
+ assertEquals(MediaType.valueOf(MediaType.APPLICATION_JSON),
response.getMediaType());
+
assertTrue(FlowComparisonEntity.class.isInstance(response.getEntity()));
+
+ final FlowComparisonEntity entity = (FlowComparisonEntity)
response.getEntity();
+ final List<DifferenceDTO> differences =
entity.getComponentDifferences().stream().map(ComponentDifferenceDTO::getDifferences).flatMap(Collection::stream).collect(Collectors.toList());
+ assertEquals(3, differences.size());
+ assertEquals(createDifference("Component Added", "Connection was
added"), differences.get(0));
+ assertEquals(createDifference("Property Value Changed", "From '0B' to
'1KB'"), differences.get(1));
+ assertEquals(createDifference("Position Changed", "Position was
changed"), differences.get(2));
+ }
+
+ @Test
+ public void testGetVersionDifferencesWithOffsetAndPartialResults() {
+ setUpGetVersionDifference();
+
+ final Response response = resource.getVersionDifferences(
+ SAMPLE_REGISTRY_ID, SAMPLE_BRANCH_ID_A, SAMPLE_BUCKET_ID_A,
SAMPLE_FLOW_ID_A, "1", SAMPLE_BRANCH_ID_B, SAMPLE_BUCKET_ID_B,
SAMPLE_FLOW_ID_B, "2", 2, 3
+ );
+
+ assertNotNull(response);
+ assertEquals(MediaType.valueOf(MediaType.APPLICATION_JSON),
response.getMediaType());
+
assertTrue(FlowComparisonEntity.class.isInstance(response.getEntity()));
+
+ final FlowComparisonEntity entity = (FlowComparisonEntity)
response.getEntity();
+ final List<DifferenceDTO> differences =
entity.getComponentDifferences().stream().map(ComponentDifferenceDTO::getDifferences).flatMap(Collection::stream).collect(Collectors.toList());
+ assertEquals(3, differences.size());
+ assertEquals(createDifference("Position Changed", "Position was
changed"), differences.get(0));
+ assertEquals(createDifference("Property Value Changed", "From 'false'
to 'true'"), differences.get(1));
+ assertEquals(createDifference("Component Added", "Processor was
added"), differences.get(2));
+ }
+
+ @Test
+ public void testGetVersionDifferencesWithOffsetAndOnlyPartialResult() {
+ setUpGetVersionDifference();
+
+ final Response response = resource.getVersionDifferences(
+ SAMPLE_REGISTRY_ID, SAMPLE_BRANCH_ID_A, SAMPLE_BUCKET_ID_A,
SAMPLE_FLOW_ID_A, "1", SAMPLE_BRANCH_ID_B, SAMPLE_BUCKET_ID_B,
SAMPLE_FLOW_ID_B, "2", 2, 1
+ );
+
+ assertNotNull(response);
+ assertEquals(MediaType.valueOf(MediaType.APPLICATION_JSON),
response.getMediaType());
+
assertTrue(FlowComparisonEntity.class.isInstance(response.getEntity()));
+
+ final FlowComparisonEntity entity = (FlowComparisonEntity)
response.getEntity();
+ final List<DifferenceDTO> differences =
entity.getComponentDifferences().stream().map(ComponentDifferenceDTO::getDifferences).flatMap(Collection::stream).collect(Collectors.toList());
+ assertEquals(1, differences.size());
+ assertEquals(createDifference("Position Changed", "Position was
changed"), differences.get(0));
+ }
+
+ private void setUpGetVersionDifference() {
+ final FlowVersionLocation baseLocation = new
FlowVersionLocation(SAMPLE_BRANCH_ID_A, SAMPLE_BUCKET_ID_A, SAMPLE_FLOW_ID_A,
"1");
+ final FlowVersionLocation comparedLocation = new
FlowVersionLocation(SAMPLE_BRANCH_ID_B, SAMPLE_BUCKET_ID_B, SAMPLE_FLOW_ID_B,
"2");
+
doReturn(getDifferences()).when(serviceFacade).getVersionDifference(anyString(),
any(FlowVersionLocation.class), any(FlowVersionLocation.class));
+ }
+
+ private static DifferenceDTO createDifference(final String type, final
String difference) {
+ final DifferenceDTO result = new DifferenceDTO();
+ result.setDifferenceType(type);
+ result.setDifference(difference);
+ return result;
+ }
+
+ private static FlowComparisonEntity getDifferences() {
+ final FlowComparisonEntity differences = new FlowComparisonEntity();
+ final Set<ComponentDifferenceDTO> componentDifferences = new
HashSet<>();
+
+ final ComponentDifferenceDTO changedComponent1 = new
ComponentDifferenceDTO();
+
changedComponent1.setComponentId("d72f9efe-506d-30e8-8a9f-257a69e73cd2");
+ changedComponent1.setComponentName("LogAttribute");
+ changedComponent1.setComponentType("Processor");
+ changedComponent1.setDifferences(List.of(createDifference("Component
Added", "Processor was added")));
+
+ final ComponentDifferenceDTO changedComponent2 = new
ComponentDifferenceDTO();
+
changedComponent2.setComponentId("46aa1d19-65ee-32f5-83dc-e14a8d3f7e7f");
+ changedComponent2.setComponentName("GenerateFlowFile");
+ changedComponent2.setComponentType("Processor");
+ changedComponent2.setDifferences(List.of(
+ createDifference("Property Value Changed", "From '0B' to '1KB'"),
+ createDifference("Position Changed", "Position was changed"),
+ createDifference("Property Value Changed", "From 'false' to
'true'")
+ ));
+
+ final ComponentDifferenceDTO changedComponent3 = new
ComponentDifferenceDTO();
+
changedComponent3.setComponentId("cfd8f7ec-3f40-3763-af15-2dc0e227ed61");
+ changedComponent3.setComponentName("");
+ changedComponent3.setComponentType("Connection");
+ changedComponent3.setDifferences(List.of(createDifference("Component
Added", "Connection was added")));
+
+ componentDifferences.add(changedComponent1);
+ componentDifferences.add(changedComponent2);
+ componentDifferences.add(changedComponent3);
+ differences.setComponentDifferences(componentDifferences);
+
+ return differences;
+ }
+
private String getResponseOutput(final Response response) throws
IOException {
final StreamingOutput streamingOutput = (StreamingOutput)
response.getEntity();
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/util/ClosedOpenIntervalTest.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/util/ClosedOpenIntervalTest.java
new file mode 100644
index 0000000000..a38475a011
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/util/ClosedOpenIntervalTest.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.util;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.stream.Stream;
+
+class ClosedOpenIntervalTest {
+
+ @Test
+ public void testNegativeLowerBoundary() {
+ Assertions.assertThrows(IllegalArgumentException.class, () -> new
ClosedOpenInterval(-1, 3));
+ }
+
+ @Test
+ public void testNegativeHigherBoundary() {
+ Assertions.assertThrows(IllegalArgumentException.class, () -> new
ClosedOpenInterval(0, -1));
+ }
+
+ @Test
+ public void testSwitchedBoundaries() {
+ Assertions.assertThrows(IllegalArgumentException.class, () -> new
ClosedOpenInterval(7, 3));
+ }
+
+ @Test
+ public void testCompareWhenOtherLowerBoundaryIsNegative() {
+ final ClosedOpenInterval testSubject = new ClosedOpenInterval(1, 3);
+ Assertions.assertThrows(IllegalArgumentException.class, () ->
testSubject.getRelativePositionOf(-1, 4));
+ }
+
+ @Test
+ public void testCompareWhenOtherBoundariesAreSwitched() {
+ final ClosedOpenInterval testSubject = new ClosedOpenInterval(1, 3);
+ Assertions.assertThrows(IllegalArgumentException.class, () ->
testSubject.getRelativePositionOf(9, 4));
+ }
+
+ @Test
+ public void testCompareWhenOtherHigherBoundaryIsUnspecified() {
+ final ClosedOpenInterval testSubject = new ClosedOpenInterval(1, 3);
+ Assertions.assertThrows(IllegalArgumentException.class, () ->
testSubject.getRelativePositionOf(2, 0));
+ }
+
+ @Test
+ public void testZeroElementInterval() {
+ Assertions.assertThrows(IllegalArgumentException.class, () -> new
ClosedOpenInterval(3, 3));
+ }
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("givenTestData")
+ public void testCheckForOverlapping(
+ final String name, final ClosedOpenInterval interval, final int
otherIntervalLowerBoundary, final int otherIntervalHigherBoundary, final
ClosedOpenInterval.RelativePosition expectedResult
+ ) {
+ Assertions.assertEquals(expectedResult,
interval.getRelativePositionOf(otherIntervalLowerBoundary,
otherIntervalHigherBoundary));
+ }
+
+ private static Stream<Arguments> givenTestData() {
+ return Stream.of(
+ // Both boundaries are defined
+ Arguments.of("Other starts after actual ends", new
ClosedOpenInterval(7, 10), 11, 13, ClosedOpenInterval.RelativePosition.AFTER),
+ Arguments.of("Other starts where actual ends", new
ClosedOpenInterval(7, 10), 10, 13, ClosedOpenInterval.RelativePosition.AFTER),
+ Arguments.of("Other starts within actual and ends after
actual", new ClosedOpenInterval(7, 10), 9, 13,
ClosedOpenInterval.RelativePosition.HEAD_INTERSECTS),
+ Arguments.of("Other starts within actual and ends where actual
ends", new ClosedOpenInterval(7, 10), 8, 10,
ClosedOpenInterval.RelativePosition.WITHIN),
+ Arguments.of("Other is contained by actual", new
ClosedOpenInterval(7, 10), 8, 9, ClosedOpenInterval.RelativePosition.WITHIN),
+ Arguments.of("Other starts where actual and ends within
actual", new ClosedOpenInterval(7, 10), 7, 12,
ClosedOpenInterval.RelativePosition.HEAD_INTERSECTS),
+ Arguments.of("Other matches actual", new ClosedOpenInterval(7,
10), 7, 10, ClosedOpenInterval.RelativePosition.WITHIN),
+ Arguments.of("Other starts where actual and finishes within",
new ClosedOpenInterval(7, 10), 7, 9,
ClosedOpenInterval.RelativePosition.WITHIN),
+ Arguments.of("Other exceeds actual in both directions", new
ClosedOpenInterval(7, 10), 6, 12, ClosedOpenInterval.RelativePosition.EXCEEDS),
+ Arguments.of("Other starts before actual and ends where actual
ends", new ClosedOpenInterval(7, 10), 5, 10,
ClosedOpenInterval.RelativePosition.TAIL_INTERSECTS),
+ Arguments.of("Other starts before actual and ends within", new
ClosedOpenInterval(7, 10), 5, 9,
ClosedOpenInterval.RelativePosition.TAIL_INTERSECTS),
+ Arguments.of("Other starts where actual ends", new
ClosedOpenInterval(7, 10), 2, 7, ClosedOpenInterval.RelativePosition.BEFORE),
+ Arguments.of("Other precedes actual interval", new
ClosedOpenInterval(7, 10), 2, 6, ClosedOpenInterval.RelativePosition.BEFORE),
+
+ // Actual has no higher boundary defined
+ Arguments.of("Fully within with no higher boundary, when start
at lower boundary", new ClosedOpenInterval(3, 0), 3, 6,
ClosedOpenInterval.RelativePosition.WITHIN),
+ Arguments.of("Fully within with no higher boundary", new
ClosedOpenInterval(3, 0), 11, 12, ClosedOpenInterval.RelativePosition.WITHIN),
+ Arguments.of("Tail is within with no higher boundary", new
ClosedOpenInterval(3, 0), 2, 4,
ClosedOpenInterval.RelativePosition.TAIL_INTERSECTS)
+ );
+ }
+}
\ No newline at end of file
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/util/PaginationHelperTest.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/util/PaginationHelperTest.java
new file mode 100644
index 0000000000..669f9105b9
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/util/PaginationHelperTest.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.util;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+class PaginationHelperTest {
+
+ @Test
+ public void testCreatingWithNegativeOffset() {
+ Assertions.assertThrows(IllegalArgumentException.class, () ->
PaginationHelper.paginateByContainedItems(getTestInput(), -1, 3,
Function.identity(), (original, partialList) -> partialList));
+ }
+
+ @Test
+ public void testCreatingWithNegativeLimit() {
+ Assertions.assertThrows(IllegalArgumentException.class, () ->
PaginationHelper.paginateByContainedItems(getTestInput(), 0, -1,
Function.identity(), (original, partialList) -> partialList));
+ }
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("givenTestData")
+ public void testPaginateByContainedItems(final String name, final int
offset, final int limit, final List<Integer> expectedResult) {
+ final List<List<Integer>> result =
PaginationHelper.paginateByContainedItems(getTestInput(), offset, limit,
Function.identity(), (original, partialList) -> partialList);
+ final List<Integer> flatten =
result.stream().flatMap(Collection::stream).collect(Collectors.toList());
+ Assertions.assertIterableEquals(expectedResult, flatten);
+ }
+
+ private static Stream<Arguments> givenTestData() {
+ return Stream.of(
+ Arguments.of("Full result set", 0, 0, Arrays.asList(1, 2, 3,
4, 5, 6, 7, 8, 9, 10, 11, 12)),
+ Arguments.of("Offset only when starts with full", 3, 0,
Arrays.asList(4, 5, 6, 7, 8, 9, 10, 11, 12)),
+ Arguments.of("Offset only when starts with partial", 4, 0,
Arrays.asList(5, 6, 7, 8, 9, 10, 11, 12)),
+ Arguments.of("From beginning with partial", 0, 5,
Arrays.asList(1, 2, 3, 4, 5)),
+ Arguments.of("Beginning with partial and offset", 1, 5,
Arrays.asList(2, 3, 4, 5, 6)),
+ Arguments.of("Middle partial only", 4, 2, Arrays.asList(5, 6)),
+ Arguments.of("From the end with partial", 9, 3,
Arrays.asList(10, 11, 12)),
+ Arguments.of("From the end with partial when spills over", 9,
5, Arrays.asList(10, 11, 12)),
+ Arguments.of("Clear cut", 3, 5, Arrays.asList(4, 5, 6, 7, 8)),
+ Arguments.of("Long result", 2, 8, Arrays.asList(3, 4, 5, 6, 7,
8, 9, 10)),
+ Arguments.of("All the original items", 0, 12, Arrays.asList(1,
2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)),
+ Arguments.of("Second partial", 5, 7, Arrays.asList(6, 7, 8, 9,
10, 11, 12)),
+ Arguments.of("From the end of the range", 12, 3,
Collections.emptyList()),
+ Arguments.of("Outside of the range", 14, 4,
Collections.emptyList())
+ );
+ }
+
+ private static List<List<Integer>> getTestInput() {
+ final List<Integer> l1 = Arrays.asList(1, 2, 3);
+ final List<Integer> l2 = Arrays.asList(4, 5, 6, 7, 8);
+ final List<Integer> l3 = Arrays.asList(9, 10, 11, 12);
+ return new ArrayList<>(Arrays.asList(l1, l2, l3));
+ }
+}
\ No newline at end of file