diff --git a/nifi-docs/src/main/asciidoc/images/import-version-dialog.png b/nifi-docs/src/main/asciidoc/images/import-version-dialog.png index c9927472ab92..a7ff4267984d 100644 Binary files a/nifi-docs/src/main/asciidoc/images/import-version-dialog.png and b/nifi-docs/src/main/asciidoc/images/import-version-dialog.png differ diff --git a/nifi-docs/src/main/asciidoc/user-guide.adoc b/nifi-docs/src/main/asciidoc/user-guide.adoc index 3c114f2e15ba..567526d6007c 100644 --- a/nifi-docs/src/main/asciidoc/user-guide.adoc +++ b/nifi-docs/src/main/asciidoc/user-guide.adoc @@ -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"]). diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ParameterContextHandlingStrategy.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ParameterContextHandlingStrategy.java new file mode 100644 index 000000000000..d1f7414fab65 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ParameterContextHandlingStrategy.java @@ -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 +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/registry/flow/StandardFlowRegistryClientNode.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/registry/flow/StandardFlowRegistryClientNode.java index c5a8f9164902..6924a1648703 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/registry/flow/StandardFlowRegistryClientNode.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/registry/flow/StandardFlowRegistryClientNode.java @@ -55,6 +55,7 @@ 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 FlowSnapshotContainer getFlowContents( if (fetchRemoteFlows) { final VersionedProcessGroup contents = flowSnapshot.getFlowContents(); for (final VersionedProcessGroup child : contents.getProcessGroups()) { + final Map childParameterContexts = populateVersionedContentsRecursively(context, child, snapshotContainer); + + for (final Map.Entry childParameterContext : childParameterContexts.entrySet()) { + flowSnapshot.getParameterContexts().putIfAbsent(childParameterContext.getKey(), childParameterContext.getValue()); + } populateVersionedContentsRecursively(context, child, snapshotContainer); } } @@ -297,10 +303,15 @@ private String extractIdentity(final FlowRegistryClientUserContext context) { return context.getNiFiUserIdentity().orElse(null); } - private void populateVersionedContentsRecursively(final FlowRegistryClientUserContext context, final VersionedProcessGroup group, - final FlowSnapshotContainer snapshotContainer) throws FlowRegistryException { + private Map populateVersionedContentsRecursively( + final FlowRegistryClientUserContext context, + final VersionedProcessGroup group, + final FlowSnapshotContainer snapshotContainer + ) throws FlowRegistryException { + Map accumulatedParameterContexts = new HashMap<>(); + if (group == null) { - return; + return accumulatedParameterContexts; } final VersionedFlowCoordinates coordinates = group.getVersionedFlowCoordinates(); @@ -330,12 +341,23 @@ private void populateVersionedContentsRecursively(final FlowRegistryClientUserCo group.setLogFileSuffix(contents.getLogFileSuffix()); coordinates.setLatest(snapshot.isLatest()); + for (final Map.Entry 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 childParameterContexts = populateVersionedContentsRecursively(context, child, snapshotContainer); + + for (final Map.Entry 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, diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java index 33744e25d7e8..925adc1bb45c 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java @@ -28,6 +28,57 @@ 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.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.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.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 varRegistryUpdateRequests = new ConcurrentHashMap<>(); private static final int MAX_VARIABLE_REGISTRY_UPDATE_REQUESTS = 100; @@ -1969,8 +1971,16 @@ public Response createProcessGroup( @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 Response createProcessGroup( } } - // 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 Response createProcessGroup( // 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 Response createProcessGroup( 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 void setControllerServiceResource(ControllerServiceResource controllerSer this.controllerServiceResource = controllerServiceResource; } + public void setParameterContextReplacer(ParameterContextReplacer parameterContextReplacer) { + this.parameterContextReplacer = parameterContextReplacer; + } + private static class DropEntity extends Entity { final String entityId; final String dropRequestId; diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/ParameterContextNameCollisionResolver.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/ParameterContextNameCollisionResolver.java new file mode 100644 index 000000000000..6c7c362a2c8c --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/ParameterContextNameCollisionResolver.java @@ -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 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 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); + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/ParameterContextReplacer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/ParameterContextReplacer.java new file mode 100644 index 000000000000..56d1a6dd148e --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/ParameterContextReplacer.java @@ -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 existingContexts) { + // We do not want to have double replacements: within the snapshot we keep the identical names identical. + final Map parameterContexts = flowSnapshot.getParameterContexts(); + final Map 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 replacement : replacements.entrySet()) { + for (final VersionedParameterContext parameterContext : parameterContexts.values()) { + final List 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 replaceParameterContexts( + final VersionedProcessGroup group, + final Map flowParameterContexts, + final Map replacements, + final Collection 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 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; + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml index 5e356da4bb74..f8a471a48468 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml @@ -381,6 +381,12 @@ + + + + + + @@ -501,6 +507,7 @@ + diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/util/ParameterContextNameCollisionResolverTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/util/ParameterContextNameCollisionResolverTest.java new file mode 100644 index 000000000000..881d972e3b3e --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/util/ParameterContextNameCollisionResolverTest.java @@ -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 EMPTY_PARAMETER_CONTEXT_SOURCE = Collections.emptySet(); + private final static Collection PARAMETER_CONTEXT_SOURCE_WITH_FIRST = Arrays.asList(getTestContext("test")); + private final static Collection PARAMETER_CONTEXT_SOURCE_WITH_SOME = + Arrays.asList(getTestContext("test"), getTestContext("test (1)"), getTestContext("test (2)")); + private final static Collection PARAMETER_CONTEXT_SOURCE_WITH_NON_CONTINUOUS = + Arrays.asList(getTestContext("test (3)"), getTestContext("test (9)")); + private final static Collection 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 parameterContexts + ) { + final ParameterContextNameCollisionResolver testSubject = new ParameterContextNameCollisionResolver(); + final String result = testSubject.resolveNameCollision(oldName, parameterContexts); + Assertions.assertEquals(expectedResult, result); + } + + private static Stream 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; + } +} \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/util/ParameterContextReplacerTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/util/ParameterContextReplacerTest.java new file mode 100644 index 000000000000..cd585c06a48e --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/util/ParameterContextReplacerTest.java @@ -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 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 parameters = replacedContext.getParameters(); + final Map 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 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 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 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 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 parameters) { + final Set contextParameters = new HashSet<>(); + + for (final Map.Entry 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; + } +} \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/import-flow-version-dialog.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/import-flow-version-dialog.jsp index e2de2d18f636..1bb1eec152e9 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/import-flow-version-dialog.jsp +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/import-flow-version-dialog.jsp @@ -45,6 +45,11 @@
+
+
+
Keep Existing Parameter Contexts
+
+
Flow Description
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css index b5b68eaaee80..f47685252347 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css @@ -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 */ diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js index 134845428c38..7c37ec72527d 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js @@ -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) {