mirror of https://github.com/apache/nifi.git
NIFI-13030 Adding endpoint for comparing versions of registered flows
This closes #8670 Signed-off-by: Peter Gyori <pgyori@apache.org>
This commit is contained in:
parent
21f0ca47b0
commit
0a5be35357
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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
|
||||
// --------------
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 final class IntervalFactory {
|
||||
private IntervalFactory() {
|
||||
// Not to be instantiated
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue