NIFI-11706 Add option to create dedicated Parameter Contexts for Imported Flows

This closes #7401

Signed-off-by: David Handermann <exceptionfactory@apache.org>
This commit is contained in:
Bence Simon 2023-06-19 13:27:22 +02:00 committed by exceptionfactory
parent 5584a1b35c
commit cadf2fb6f2
No known key found for this signature in database
GPG Key ID: 29B6A52D2AAE8DBA
13 changed files with 597 additions and 64 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 105 KiB

View File

@ -2440,6 +2440,10 @@ image::import-version-dialog.png["Import Version Dialog"]
Connected registries will appear as options in the Registry drop-down menu. For the chosen Registry, buckets the user has access to will appear as options in the Bucket drop-down menu. The names of the flows in the chosen bucket will appear as options in the Name drop-down menu. Select the desired version of the flow to import and select "Import" for the dataflow to be placed on the canvas.
The import also provides the option to keep or replace existing Parameter Contexts based on name. Keeping the Parameter Contexts (which is the default behaviour) will use the existing Contexts if Contexts with the same name already exists, resulting shared parameter sets between multiple imports.
Unchecking the checkbox named "Keep Existing Parameter Contexts" will result the creation of a new set of Parameter Contexts for the import, making it completely independent of the existing imports. The parameter values of these new Contexts will be set based on the content of the Registry Snapshot.
image::versioned-flow-imported.png["Versioned Flow Imported"]
Since the version imported in this example is the latest version (MySQL CDC, Version 3), the state of the versioned process group is "Up to date" (image:iconUpToDate.png["Up To Date Icon"]). If the version imported had been an older version, the state would be "Stale" (image:iconStale.png["Stale Icon"]).

View File

@ -0,0 +1,21 @@
/*
* 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.api.dto;
public enum ParameterContextHandlingStrategy {
KEEP_EXISTING, REPLACE
}

View File

@ -55,6 +55,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -225,6 +226,11 @@ public final class StandardFlowRegistryClientNode extends AbstractComponentNode
if (fetchRemoteFlows) {
final VersionedProcessGroup contents = flowSnapshot.getFlowContents();
for (final VersionedProcessGroup child : contents.getProcessGroups()) {
final Map<String, VersionedParameterContext> childParameterContexts = populateVersionedContentsRecursively(context, child, snapshotContainer);
for (final Map.Entry<String, VersionedParameterContext> childParameterContext : childParameterContexts.entrySet()) {
flowSnapshot.getParameterContexts().putIfAbsent(childParameterContext.getKey(), childParameterContext.getValue());
}
populateVersionedContentsRecursively(context, child, snapshotContainer);
}
}
@ -297,10 +303,15 @@ public final class StandardFlowRegistryClientNode extends AbstractComponentNode
return context.getNiFiUserIdentity().orElse(null);
}
private void populateVersionedContentsRecursively(final FlowRegistryClientUserContext context, final VersionedProcessGroup group,
final FlowSnapshotContainer snapshotContainer) throws FlowRegistryException {
private Map<String, VersionedParameterContext> populateVersionedContentsRecursively(
final FlowRegistryClientUserContext context,
final VersionedProcessGroup group,
final FlowSnapshotContainer snapshotContainer
) throws FlowRegistryException {
Map<String, VersionedParameterContext> accumulatedParameterContexts = new HashMap<>();
if (group == null) {
return;
return accumulatedParameterContexts;
}
final VersionedFlowCoordinates coordinates = group.getVersionedFlowCoordinates();
@ -330,12 +341,23 @@ public final class StandardFlowRegistryClientNode extends AbstractComponentNode
group.setLogFileSuffix(contents.getLogFileSuffix());
coordinates.setLatest(snapshot.isLatest());
for (final Map.Entry<String, VersionedParameterContext> parameterContext : snapshot.getParameterContexts().entrySet()) {
accumulatedParameterContexts.put(parameterContext.getKey(), parameterContext.getValue());
}
snapshotContainer.addChildSnapshot(snapshot, group);
}
for (final VersionedProcessGroup child : group.getProcessGroups()) {
populateVersionedContentsRecursively(context, child, snapshotContainer);
final Map<String, VersionedParameterContext> childParameterContexts = populateVersionedContentsRecursively(context, child, snapshotContainer);
for (final Map.Entry<String, VersionedParameterContext> childParameterContext : childParameterContexts.entrySet()) {
// We favor the context instance from the enclosing versioned flow
accumulatedParameterContexts.putIfAbsent(childParameterContext.getKey(), childParameterContext.getValue());
}
}
return accumulatedParameterContexts;
}
private RegisteredFlowSnapshot fetchFlowContents(final FlowRegistryClientUserContext context, final VersionedFlowCoordinates coordinates,

View File

@ -28,6 +28,57 @@ import io.swagger.annotations.ApiParam;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import io.swagger.annotations.Authorization;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.stream.XMLStreamReader;
import javax.xml.transform.stream.StreamSource;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringEscapeUtils;
import org.apache.nifi.authorization.AuthorizableLookup;
@ -73,6 +124,7 @@ import org.apache.nifi.web.api.dto.ConnectionDTO;
import org.apache.nifi.web.api.dto.ControllerServiceDTO;
import org.apache.nifi.web.api.dto.DropRequestDTO;
import org.apache.nifi.web.api.dto.FlowSnippetDTO;
import org.apache.nifi.web.api.dto.ParameterContextHandlingStrategy;
import org.apache.nifi.web.api.dto.PortDTO;
import org.apache.nifi.web.api.dto.PositionDTO;
import org.apache.nifi.web.api.dto.ProcessGroupDTO;
@ -123,6 +175,7 @@ import org.apache.nifi.web.api.entity.VariableRegistryUpdateRequestEntity;
import org.apache.nifi.web.api.request.ClientIdParameter;
import org.apache.nifi.web.api.request.LongParameter;
import org.apache.nifi.web.security.token.NiFiAuthenticationToken;
import org.apache.nifi.web.util.ParameterContextReplacer;
import org.apache.nifi.web.util.Pause;
import org.apache.nifi.xml.processing.stream.StandardXMLStreamReaderProvider;
import org.apache.nifi.xml.processing.stream.XMLStreamReaderProvider;
@ -132,58 +185,6 @@ import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.stream.XMLStreamReader;
import javax.xml.transform.stream.StreamSource;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* RESTful endpoint for managing a Group.
*/
@ -205,6 +206,7 @@ public class ProcessGroupResource extends FlowUpdateResource<ProcessGroupImportE
private ConnectionResource connectionResource;
private TemplateResource templateResource;
private ControllerServiceResource controllerServiceResource;
private ParameterContextReplacer parameterContextReplacer;
private final ConcurrentMap<String, VariableRegistryUpdateRequest> varRegistryUpdateRequests = new ConcurrentHashMap<>();
private static final int MAX_VARIABLE_REGISTRY_UPDATE_REQUESTS = 100;
@ -1969,8 +1971,16 @@ public class ProcessGroupResource extends FlowUpdateResource<ProcessGroupImportE
@ApiParam(
value = "The process group configuration details.",
required = true
) final ProcessGroupEntity requestProcessGroupEntity) {
)
final ProcessGroupEntity requestProcessGroupEntity,
@ApiParam(
value = "Handling Strategy controls whether to keep or replace Parameter Contexts",
defaultValue = "KEEP_EXISTING"
)
@QueryParam("parameterContextHandlingStrategy")
@DefaultValue("KEEP_EXISTING")
final ParameterContextHandlingStrategy parameterContextHandlingStrategy
) {
if (requestProcessGroupEntity == null || requestProcessGroupEntity.getComponent() == null) {
throw new IllegalArgumentException("Process group details must be specified.");
}
@ -2024,7 +2034,12 @@ public class ProcessGroupResource extends FlowUpdateResource<ProcessGroupImportE
}
}
// Step 4: Resolve Bundle info
// Step 4: Replace parameter contexts if necessary
if (ParameterContextHandlingStrategy.REPLACE.equals(parameterContextHandlingStrategy)) {
parameterContextReplacer.replaceParameterContexts(flowSnapshot, serviceFacade.getParameterContexts());
}
// Step 5: Resolve Bundle info
serviceFacade.discoverCompatibleBundles(flowSnapshot.getFlowContents());
// If there are any Controller Services referenced that are inherited from the parent group, resolve those to point to the appropriate Controller Service, if we are able to.
@ -2033,7 +2048,7 @@ public class ProcessGroupResource extends FlowUpdateResource<ProcessGroupImportE
// If there are any Parameter Providers referenced by Parameter Contexts, resolve these to point to the appropriate Parameter Provider, if we are able to.
serviceFacade.resolveParameterProviders(flowSnapshot, NiFiUserUtils.getNiFiUser());
// Step 5: Update contents of the ProcessGroupDTO passed in to include the components that need to be added.
// Step 6: Update contents of the ProcessGroupDTO passed in to include the components that need to be added.
requestProcessGroupEntity.setVersionedFlowSnapshot(flowSnapshot);
}
@ -2042,7 +2057,7 @@ public class ProcessGroupResource extends FlowUpdateResource<ProcessGroupImportE
serviceFacade.verifyImportProcessGroup(versionControlInfo, flowSnapshot.getFlowContents(), groupId);
}
// Step 6: Replicate the request or call serviceFacade.updateProcessGroup
// Step 7: Replicate the request or call serviceFacade.updateProcessGroup
if (isReplicateRequest()) {
return replicate(HttpMethod.POST, requestProcessGroupEntity);
} else if (isDisconnectedFromCluster()) {
@ -4787,6 +4802,10 @@ public class ProcessGroupResource extends FlowUpdateResource<ProcessGroupImportE
this.controllerServiceResource = controllerServiceResource;
}
public void setParameterContextReplacer(ParameterContextReplacer parameterContextReplacer) {
this.parameterContextReplacer = parameterContextReplacer;
}
private static class DropEntity extends Entity {
final String entityId;
final String dropRequestId;

View File

@ -0,0 +1,74 @@
/*
* 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.apache.nifi.web.api.dto.ParameterContextDTO;
import org.apache.nifi.web.api.entity.ParameterContextEntity;
import java.util.Collection;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
class ParameterContextNameCollisionResolver {
private static final String PATTERN_GROUP_NAME = "name";
private static final String PATTERN_GROUP_INDEX = "index";
private static final String LINEAGE_FORMAT = "^(?<" + PATTERN_GROUP_NAME + ">.+?)( \\((?<" + PATTERN_GROUP_INDEX + ">[0-9]+)\\))?$";
private static final Pattern LINEAGE_PATTERN = Pattern.compile(LINEAGE_FORMAT);
private static final String NAME_FORMAT = "%s (%d)";
public String resolveNameCollision(final String originalParameterContextName, final Collection<ParameterContextEntity> existingContexts) {
final Matcher lineageMatcher = LINEAGE_PATTERN.matcher(originalParameterContextName);
if (!lineageMatcher.matches()) {
throw new IllegalArgumentException("Existing Parameter Context name \"(" + originalParameterContextName + "\") cannot be processed");
}
final String lineName = lineageMatcher.group(PATTERN_GROUP_NAME);
final String originalIndex = lineageMatcher.group(PATTERN_GROUP_INDEX);
// Candidates cannot be cached because new context might be added between calls
final Set<ParameterContextDTO> candidates = existingContexts
.stream()
.map(pc -> pc.getComponent())
.filter(dto -> dto.getName().startsWith(lineName))
.collect(Collectors.toSet());
int biggestIndex = (originalIndex == null) ? 0 : Integer.valueOf(originalIndex);
for (final ParameterContextDTO candidate : candidates) {
final Matcher matcher = LINEAGE_PATTERN.matcher(candidate.getName());
if (matcher.matches() && lineName.equals(matcher.group(PATTERN_GROUP_NAME))) {
final String indexGroup = matcher.group(PATTERN_GROUP_INDEX);
if (indexGroup != null) {
int biggestIndexCandidate = Integer.valueOf(indexGroup);
if (biggestIndexCandidate > biggestIndex) {
biggestIndex = biggestIndexCandidate;
}
}
}
}
return String.format(NAME_FORMAT, lineName, biggestIndex + 1);
}
}

View File

@ -0,0 +1,125 @@
/*
* 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.Collection;
import org.apache.nifi.flow.VersionedParameterContext;
import org.apache.nifi.flow.VersionedProcessGroup;
import org.apache.nifi.registry.flow.RegisteredFlowSnapshot;
import org.apache.nifi.web.api.entity.ParameterContextEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* Replaces Parameter Contexts within the snapshot following name conventions.
*
* A set of Parameter Contexts following a given name convention is considered as a lineage. Lineage is used to
* group Parameter Contexts and determine a non-conflicting name for the newly created replacements. This class
* creates replaces all Parameter Contexts in the snapshot, keeping care of the lineage for the given Contexts.
*
* Note: if multiple (sub)groups refer to the same Parameter Context, only one replacement will be created and all
* Process Groups referred to the original Parameter Context will refer to this replacement.
*/
public class ParameterContextReplacer {
private static final Logger LOGGER = LoggerFactory.getLogger(ParameterContextReplacer.class);
private final ParameterContextNameCollisionResolver nameCollisionResolver;
ParameterContextReplacer(final ParameterContextNameCollisionResolver nameCollisionResolver) {
this.nameCollisionResolver = nameCollisionResolver;
}
/**
* Goes through the Process Group structure and replaces Parameter Contexts to avoid collision with the ones
* existing in the flow, based on name. The method disregards if the given Parameter Context has no matching
* counterpart in the existing flow, it replaces all with newly created contexts.
*
* @param flowSnapshot Snapshot from the Registry. Modification will be applied on this object!
*/
public void replaceParameterContexts(final RegisteredFlowSnapshot flowSnapshot, final Collection<ParameterContextEntity> existingContexts) {
// We do not want to have double replacements: within the snapshot we keep the identical names identical.
final Map<String, VersionedParameterContext> parameterContexts = flowSnapshot.getParameterContexts();
final Map<String, VersionedParameterContext> replacements = replaceParameterContexts(flowSnapshot.getFlowContents(), parameterContexts, new HashMap<>(), existingContexts);
// This is needed because if a PC is used for both assignments and inheritance (parent) then we would both change it
// but without updating the inheritance reference. {@see NIFI-11706 TC#8}
for (final Map.Entry<String, VersionedParameterContext> replacement : replacements.entrySet()) {
for (final VersionedParameterContext parameterContext : parameterContexts.values()) {
final List<String> inheritedContexts = parameterContext.getInheritedParameterContexts();
if (inheritedContexts.contains(replacement.getKey())) {
inheritedContexts.remove(replacement.getKey());
inheritedContexts.add(replacement.getValue().getName());
}
}
}
}
/**
* @return A collection of replaced Parameter Contexts. Every map entry represents a singe replacement where the key
* is the old context's name and the value is the new context.
*/
private Map<String, VersionedParameterContext> replaceParameterContexts(
final VersionedProcessGroup group,
final Map<String, VersionedParameterContext> flowParameterContexts,
final Map<String, VersionedParameterContext> replacements,
final Collection<ParameterContextEntity> existingContexts
) {
if (group.getParameterContextName() != null) {
final String oldParameterContextName = group.getParameterContextName();
final VersionedParameterContext oldParameterContext = flowParameterContexts.get(oldParameterContextName);
if (replacements.containsKey(oldParameterContextName)) {
final String replacementContextName = replacements.get(oldParameterContextName).getName();
group.setParameterContextName(replacementContextName);
LOGGER.debug("Replacing Parameter Context in Group {} from {} into {}", group.getIdentifier(), oldParameterContext, replacementContextName);
} else {
final VersionedParameterContext replacementContext = createReplacementContext(oldParameterContext, existingContexts);
group.setParameterContextName(replacementContext.getName());
flowParameterContexts.remove(oldParameterContextName);
flowParameterContexts.put(replacementContext.getName(), replacementContext);
replacements.put(oldParameterContextName, replacementContext);
LOGGER.debug("Replacing Parameter Context in Group {} from {} into the newly created {}", group.getIdentifier(), oldParameterContext, replacementContext.getName());
}
}
for (final VersionedProcessGroup childGroup : group.getProcessGroups()) {
replaceParameterContexts(childGroup, flowParameterContexts, replacements, existingContexts);
}
return replacements;
}
private VersionedParameterContext createReplacementContext(final VersionedParameterContext original, final Collection<ParameterContextEntity> existingContexts) {
final VersionedParameterContext replacement = new VersionedParameterContext();
replacement.setName(nameCollisionResolver.resolveNameCollision(original.getName(), existingContexts));
replacement.setParameters(new HashSet<>(original.getParameters()));
replacement.setInheritedParameterContexts(Optional.ofNullable(original.getInheritedParameterContexts()).orElse(new ArrayList<>()));
replacement.setDescription(original.getDescription());
replacement.setSynchronized(original.isSynchronized());
replacement.setParameterProvider(original.getParameterProvider());
replacement.setParameterGroupName(original.getParameterGroupName());
return replacement;
}
}

View File

@ -381,6 +381,12 @@
<property name="variableRegistry" ref="variableRegistry"/>
</bean>
<bean id="nameCollisionResolver" class="org.apache.nifi.web.util.ParameterContextNameCollisionResolver" />
<bean id="parameterContextReplacer" class="org.apache.nifi.web.util.ParameterContextReplacer">
<constructor-arg ref="nameCollisionResolver" />
</bean>
<!-- rest endpoints -->
<bean id="flowResource" class="org.apache.nifi.web.api.FlowResource" scope="singleton">
<property name="serviceFacade" ref="serviceFacade"/>
@ -501,6 +507,7 @@
<property name="clusterComponentLifecycle" ref="clusterComponentLifecycle" />
<property name="localComponentLifecycle" ref="localComponentLifecycle" />
<property name="dtoFactory" ref="dtoFactory" />
<property name="parameterContextReplacer" ref="parameterContextReplacer" />
</bean>
<bean id="versionsResource" class="org.apache.nifi.web.api.VersionsResource" scope="singleton">
<property name="serviceFacade" ref="serviceFacade" />

View File

@ -0,0 +1,87 @@
/*
* 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.apache.nifi.web.api.dto.ParameterContextDTO;
import org.apache.nifi.web.api.entity.ParameterContextEntity;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mockito;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.stream.Stream;
class ParameterContextNameCollisionResolverTest {
private final static Collection<ParameterContextEntity> EMPTY_PARAMETER_CONTEXT_SOURCE = Collections.emptySet();
private final static Collection<ParameterContextEntity> PARAMETER_CONTEXT_SOURCE_WITH_FIRST = Arrays.asList(getTestContext("test"));
private final static Collection<ParameterContextEntity> PARAMETER_CONTEXT_SOURCE_WITH_SOME =
Arrays.asList(getTestContext("test"), getTestContext("test (1)"), getTestContext("test (2)"));
private final static Collection<ParameterContextEntity> PARAMETER_CONTEXT_SOURCE_WITH_NON_CONTINUOUS =
Arrays.asList(getTestContext("test (3)"), getTestContext("test (9)"));
private final static Collection<ParameterContextEntity> PARAMETER_CONTEXT_SOURCE_WITH_OTHER_LINEAGES =
Arrays.asList(getTestContext("test"), getTestContext("test2 (3)"), getTestContext("other"));
@ParameterizedTest(name = "\"{0}\" into \"{1}\"")
@MethodSource("testDataSet")
public void testResolveNameCollision(
final String oldName,
final String expectedResult,
final Collection<ParameterContextEntity> parameterContexts
) {
final ParameterContextNameCollisionResolver testSubject = new ParameterContextNameCollisionResolver();
final String result = testSubject.resolveNameCollision(oldName, parameterContexts);
Assertions.assertEquals(expectedResult, result);
}
private static Stream<Arguments> testDataSet() {
return Stream.of(
Arguments.of("test", "test (1)", EMPTY_PARAMETER_CONTEXT_SOURCE),
Arguments.of("test (1)", "test (2)", EMPTY_PARAMETER_CONTEXT_SOURCE),
Arguments.of("test(1)", "test(1) (1)", EMPTY_PARAMETER_CONTEXT_SOURCE),
Arguments.of("test (1) (1)", "test (1) (2)", EMPTY_PARAMETER_CONTEXT_SOURCE),
Arguments.of("(1)", "(1) (1)", EMPTY_PARAMETER_CONTEXT_SOURCE),
Arguments.of(
"((((Lorem.ipsum dolor sit.amet, consectetur adipiscing elit",
"((((Lorem.ipsum dolor sit.amet, consectetur adipiscing elit (1)",
EMPTY_PARAMETER_CONTEXT_SOURCE),
Arguments.of("test", "test (1)", PARAMETER_CONTEXT_SOURCE_WITH_FIRST),
Arguments.of("test (1)", "test (2)", PARAMETER_CONTEXT_SOURCE_WITH_FIRST),
Arguments.of("test (8)", "test (9)", PARAMETER_CONTEXT_SOURCE_WITH_FIRST),
Arguments.of("other", "other (1)", PARAMETER_CONTEXT_SOURCE_WITH_FIRST),
Arguments.of("test", "test (3)", PARAMETER_CONTEXT_SOURCE_WITH_SOME),
Arguments.of("test (1)", "test (3)", PARAMETER_CONTEXT_SOURCE_WITH_SOME),
Arguments.of("other", "other (1)", PARAMETER_CONTEXT_SOURCE_WITH_SOME),
Arguments.of("test", "test (10)", PARAMETER_CONTEXT_SOURCE_WITH_NON_CONTINUOUS),
Arguments.of("test (3)", "test (10)", PARAMETER_CONTEXT_SOURCE_WITH_NON_CONTINUOUS),
Arguments.of("test (15)", "test (16)", PARAMETER_CONTEXT_SOURCE_WITH_NON_CONTINUOUS),
Arguments.of("test", "test (1)", PARAMETER_CONTEXT_SOURCE_WITH_OTHER_LINEAGES),
Arguments.of("test (1)", "test (2)", PARAMETER_CONTEXT_SOURCE_WITH_OTHER_LINEAGES)
);
}
private static ParameterContextEntity getTestContext(final String name) {
final ParameterContextEntity result = Mockito.mock(ParameterContextEntity.class);
final ParameterContextDTO dto = Mockito.mock(ParameterContextDTO.class);
Mockito.when(result.getComponent()).thenReturn(dto);
Mockito.when(dto.getName()).thenReturn(name);
return result;
}
}

View File

@ -0,0 +1,145 @@
/*
* 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.apache.nifi.flow.VersionedParameter;
import org.apache.nifi.flow.VersionedParameterContext;
import org.apache.nifi.flow.VersionedProcessGroup;
import org.apache.nifi.registry.flow.RegisteredFlowSnapshot;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
class ParameterContextReplacerTest {
private static final String CONTEXT_ONE_NAME = "contextOne";
private static final String CONTEXT_TWO_NAME = "contextTwo (1)";
private static final String CONTEXT_ONE_NAME_AFTER_REPLACE = "contextOne (1)";
private static final String CONTEXT_TWO_NAME_AFTER_REPLACE = "contextTwo (2)";
@Test
public void testReplacementWithoutSubgroups() {
final ParameterContextNameCollisionResolver collisionResolver = new ParameterContextNameCollisionResolver();
final ParameterContextReplacer testSubject = new ParameterContextReplacer(collisionResolver);
final RegisteredFlowSnapshot snapshot = getSimpleSnapshot();
testSubject.replaceParameterContexts(snapshot, new HashSet<>());
final Map<String, VersionedParameterContext> parameterContexts = snapshot.getParameterContexts();
Assertions.assertEquals(1, parameterContexts.size());
Assertions.assertTrue(parameterContexts.containsKey(CONTEXT_ONE_NAME_AFTER_REPLACE));
final VersionedParameterContext replacedContext = parameterContexts.get(CONTEXT_ONE_NAME_AFTER_REPLACE);
final Set<VersionedParameter> parameters = replacedContext.getParameters();
final Map<String, VersionedParameter> parametersByName = new HashMap<>();
for (final VersionedParameter parameter : parameters) {
parametersByName.put(parameter.getName(), parameter);
}
Assertions.assertEquals(CONTEXT_ONE_NAME_AFTER_REPLACE, snapshot.getFlowContents().getParameterContextName());
Assertions.assertEquals("value1", parametersByName.get("param1").getValue());
Assertions.assertEquals("value2", parametersByName.get("param2").getValue());
}
@Test
public void testReplacementWithSubgroups() {
final ParameterContextNameCollisionResolver collisionResolver = new ParameterContextNameCollisionResolver();
final ParameterContextReplacer testSubject = new ParameterContextReplacer(collisionResolver);
final RegisteredFlowSnapshot snapshot = getMultiLevelSnapshot();
testSubject.replaceParameterContexts(snapshot, new HashSet<>());
final Map<String, VersionedParameterContext> parameterContexts = snapshot.getParameterContexts();
Assertions.assertEquals(2, parameterContexts.size());
Assertions.assertTrue(parameterContexts.containsKey(CONTEXT_ONE_NAME_AFTER_REPLACE));
Assertions.assertTrue(parameterContexts.containsKey(CONTEXT_TWO_NAME_AFTER_REPLACE));
}
private static RegisteredFlowSnapshot getSimpleSnapshot() {
final VersionedProcessGroup processGroup = getProcessGroup("PG1", CONTEXT_ONE_NAME);
final RegisteredFlowSnapshot snapshot = new RegisteredFlowSnapshot();
snapshot.setParameterContexts(new HashMap<>(Collections.singletonMap(CONTEXT_ONE_NAME, getParameterContextOne())));
snapshot.setFlowContents(processGroup);
return snapshot;
}
private static RegisteredFlowSnapshot getMultiLevelSnapshot() {
final VersionedProcessGroup childProcessGroup1 = getProcessGroup("CPG1", CONTEXT_ONE_NAME);
final VersionedProcessGroup childProcessGroup2 = getProcessGroup("CPG2", CONTEXT_TWO_NAME);
final VersionedProcessGroup processGroup = getProcessGroup("PG1", CONTEXT_TWO_NAME, childProcessGroup1, childProcessGroup2);
final Map<String, VersionedParameterContext> parameterContexts = new HashMap<>();
parameterContexts.put(CONTEXT_ONE_NAME, getParameterContextOne());
parameterContexts.put(CONTEXT_TWO_NAME, getParameterContextTwo());
final RegisteredFlowSnapshot snapshot = new RegisteredFlowSnapshot();
snapshot.setParameterContexts(parameterContexts);
snapshot.setFlowContents(processGroup);
return snapshot;
}
private static VersionedProcessGroup getProcessGroup(final String name, final String parameterContext, final VersionedProcessGroup... children) {
final VersionedProcessGroup result = new VersionedProcessGroup();
result.setName(name);
result.setIdentifier(name); // Needed for equals check
result.setParameterContextName(parameterContext);
result.setProcessGroups(new HashSet<>(Arrays.asList(children)));
return result;
}
private static VersionedParameterContext getParameterContextOne() {
final Map<String, String> parameters = new HashMap<>();
parameters.put("param1", "value1");
parameters.put("param2", "value2");
final VersionedParameterContext context = getParameterContext(CONTEXT_ONE_NAME, parameters);
return context;
}
private static VersionedParameterContext getParameterContextTwo() {
final Map<String, String> parameters = new HashMap<>();
parameters.put("param3", "value3");
parameters.put("param4", "value4");
final VersionedParameterContext context = getParameterContext(CONTEXT_TWO_NAME, parameters);
return context;
}
public static VersionedParameterContext getParameterContext(final String name, final Map<String, String> parameters) {
final Set<VersionedParameter> contextParameters = new HashSet<>();
for (final Map.Entry<String, String> parameter : parameters.entrySet()) {
final VersionedParameter versionedParameter = new VersionedParameter();
versionedParameter.setName(parameter.getKey());
versionedParameter.setValue(parameter.getValue());
contextParameters.add(versionedParameter);
}
final VersionedParameterContext context = new VersionedParameterContext();
context.setName(name);
context.setParameters(contextParameters);
return context;
}
}

View File

@ -45,6 +45,11 @@
<div id="import-flow-version-label"></div>
</div>
</div>
<div class="setting keep-parameter-context">
<div id="keepExistingParameterContext" class="nf-checkbox checkbox-checked"></div>
<div class="nf-checkbox-label">Keep Existing Parameter Contexts</div>
<div class="fa fa-question-circle" alt="Info" title="When not selected, only directly associated Parameter Contexts will be copied, inherited Contexts with no direct assignment to a Process Group are ignored."></div>
</div>
<div class="setting">
<div class="setting-name">Flow Description</div>
<div class="setting-field" id="import-flow-description-container">

View File

@ -247,11 +247,11 @@ div.progress-label {
#import-flow-version-table {
overflow: hidden;
position: absolute;
top: 270px;
top: 307px;
left: 0px;
right: 0px;
bottom: 0px;
height: 155px;
height: 118px;
}
#import-flow-description-container {
@ -276,6 +276,24 @@ div.progress-label {
border-radius: 0;
}
#import-flow-version-dialog .keep-parameter-context {
align-items: center;
display: flex;
}
.keep-parameter-context .nf-checkbox-label {
font-size: 12px;
font-weight: 500;
font-family: Roboto Slab;
}
.keep-parameter-context .fa {
color: #004849;
font-size: 12px;
line-height: 22px;
margin-left: 5px;
}
/*
Local changes
*/

View File

@ -815,6 +815,9 @@
}
}]).modal('show');
// resetting the checkbox
$('#keepExistingParameterContext').removeClass('checkbox-unchecked').addClass('checkbox-checked');
// hide the new process group dialog
$('#new-process-group-dialog').modal('hide');
});
@ -1045,7 +1048,10 @@
return $.ajax({
type: 'POST',
data: JSON.stringify(processGroupEntity),
url: '../nifi-api/process-groups/' + encodeURIComponent(nfCanvasUtils.getGroupId()) + '/process-groups',
url: '../nifi-api/process-groups/' + encodeURIComponent(nfCanvasUtils.getGroupId()) + '/process-groups?'
+ $.param({
'parameterContextHandlingStrategy' : $('#keepExistingParameterContext').hasClass('checkbox-checked') ? 'KEEP_EXISTING' : 'REPLACE'
}),
dataType: 'json',
contentType: 'application/json'
}).done(function (response) {