exceptionfactory commented on code in PR #5369:
URL: https://github.com/apache/nifi/pull/5369#discussion_r955359353


##########
nifi-api/src/main/java/org/apache/nifi/flow/VersionedParameterContext.java:
##########
@@ -25,6 +25,9 @@ public class VersionedParameterContext extends 
VersionedComponent {
     private Set<VersionedParameter> parameters;
     private List<String> inheritedParameterContexts;
     private String description;
+    private String parameterProvider;
+    private String parameterGroupName;
+    private Boolean isSynchronized;

Review Comment:
   Recommend naming this property `synchronized` to align with Bean property 
naming conventions.



##########
nifi-mock/src/main/java/org/apache/nifi/util/MockParameterProviderInitializationContext.java:
##########
@@ -0,0 +1,89 @@
+/*
+ * 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.util;
+
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.controller.ControllerServiceLookup;
+import org.apache.nifi.controller.NodeTypeProvider;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.parameter.ParameterProviderInitializationContext;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+
+public class MockParameterProviderInitializationContext extends 
MockControllerServiceLookup implements ParameterProviderInitializationContext, 
ControllerServiceLookup {
+
+    private final String identifier;
+    private final String name;
+    private final Map<PropertyDescriptor, String> properties = new HashMap<>();
+    private final ComponentLog logger;
+
+    public MockParameterProviderInitializationContext(final String identifier, 
final String name, final ComponentLog logger) {
+        this.identifier = identifier;
+        this.name = name;
+        this.logger = logger;
+    }
+
+    @Override
+    public String getIdentifier() {
+        return identifier;
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    public void setProperty(final String propertyName, final String value) {
+        setProperty(new 
PropertyDescriptor.Builder().name(propertyName).build(), value);
+    }
+
+    public void setProperty(final PropertyDescriptor propertyName, final 
String value) {
+        this.properties.put(propertyName, value);
+    }
+
+    public void setProperties(final Map<PropertyDescriptor, String> 
properties) {
+        this.properties.clear();
+        this.properties.putAll(properties);
+    }
+
+    @Override
+    public NodeTypeProvider getNodeTypeProvider() {
+        return null;
+    }
+
+    @Override
+    public ComponentLog getLogger() {
+        return logger;
+    }
+
+    @Override
+    public String getKerberosServicePrincipal() {
+        return null; //this needs to be wired in.

Review Comment:
   These comments are not quite clear, perhaps they should be removed, or 
expanded explaining expected implementation.



##########
nifi-api/src/main/java/org/apache/nifi/parameter/ParameterGroupConfiguration.java:
##########
@@ -0,0 +1,100 @@
+/*
+ * 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.parameter;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * User-provided configuration for a group of parameters fetched from a 
ParameterProvider.
+ */
+public class ParameterGroupConfiguration implements 
Comparable<ParameterGroupConfiguration> {
+
+    private final String groupName;
+
+    private final String parameterContextName;
+
+    private final Map<String, ParameterSensitivity> parameterSensitivities;
+
+    private final Boolean isSynchronized;
+
+    /**
+     * Creates a named group of parameter names.
+     * @param groupName The parameter group name
+     * @param parameterContextName The parameter context name to which 
parameters will be applied
+     * @param parameterSensitivities A map from parameter name to desired 
sensitivity.  Any parameter not included in this map will not be included
+     *                               when applied to the parameter context.
+     * @param isSynchronized If true, indicates that a ParameterContext should 
be created if not already existing, or updated if existing
+     */
+    public ParameterGroupConfiguration(final String groupName, final String 
parameterContextName, final Map<String, ParameterSensitivity> 
parameterSensitivities,
+                                       final Boolean isSynchronized) {

Review Comment:
   ```suggestion
                                          final Boolean synchronized) {
   ```



##########
nifi-api/src/main/java/org/apache/nifi/parameter/ParameterGroupConfiguration.java:
##########
@@ -0,0 +1,100 @@
+/*
+ * 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.parameter;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * User-provided configuration for a group of parameters fetched from a 
ParameterProvider.
+ */
+public class ParameterGroupConfiguration implements 
Comparable<ParameterGroupConfiguration> {
+
+    private final String groupName;
+
+    private final String parameterContextName;
+
+    private final Map<String, ParameterSensitivity> parameterSensitivities;
+
+    private final Boolean isSynchronized;

Review Comment:
   Recommend renaming to `synchronized`. Should this property be nullable, or 
should it a primitive and default to `false`?



##########
.java-version:
##########
@@ -0,0 +1 @@
+1.8

Review Comment:
   This file should be removed.



##########
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/ParameterGroupConfigurationEntity.java:
##########
@@ -0,0 +1,100 @@
+/*
+ * 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.entity;
+
+import io.swagger.annotations.ApiModelProperty;
+import org.apache.nifi.parameter.ParameterSensitivity;
+
+import javax.xml.bind.annotation.XmlType;
+import java.util.Map;
+
+/**
+ * Entity encapsulating the configuration for a single parameter group.
+ */
+@XmlType(name = "parameterGroupConfiguration")
+public class ParameterGroupConfigurationEntity extends Entity implements 
Comparable<ParameterGroupConfigurationEntity> {
+
+    private String groupName;
+    private String parameterContextName;
+    private Boolean isSynchronized;

Review Comment:
   ```suggestion
       private Boolean synchronized;
   ```



##########
nifi-api/src/main/java/org/apache/nifi/parameter/ParameterProviderInitializationContext.java:
##########
@@ -0,0 +1,47 @@
+/*
+ * 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.parameter;
+
+import org.apache.nifi.controller.NodeTypeProvider;
+import org.apache.nifi.kerberos.KerberosContext;
+import org.apache.nifi.logging.ComponentLog;
+
+public interface ParameterProviderInitializationContext extends 
KerberosContext {

Review Comment:
   Is it necessary to extend `KerberosContext`?



##########
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ParameterProviderConfigurationDTO.java:
##########
@@ -0,0 +1,65 @@
+/*
+ * 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;
+
+import io.swagger.annotations.ApiModelProperty;
+
+import javax.xml.bind.annotation.XmlType;
+
+@XmlType(name = "parameterProviderConfiguration")
+public class ParameterProviderConfigurationDTO {
+    private String parameterProviderId;
+    private String parameterProviderName;
+    private String parameterGroupName;
+    private Boolean isSynchronized;

Review Comment:
   ```suggestion
       private Boolean synchronized;
   ```



##########
nifi-api/src/main/java/org/apache/nifi/parameter/ParameterProvider.java:
##########
@@ -0,0 +1,88 @@
+/*
+ * 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.parameter;
+
+import org.apache.nifi.annotation.lifecycle.OnConfigurationRestored;
+import org.apache.nifi.components.ConfigurableComponent;
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.reporting.InitializationException;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Defines a provider that is responsible for fetching from an external source 
Parameters with
+ * which a ParameterContext can be populated.
+ *
+ * <p>
+ * <code>ParameterProvider</code>s are discovered using Java's
+ * <code>ServiceLoader</code> mechanism. As a result, all implementations must
+ * follow these rules:

Review Comment:
   With internal changes to bundle management and loading, it follows 
ServiceLoader conventions, but does not actually use the ServiceLoader 
implementation, so it would be best to adjust the wording accordingly.



##########
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/parameter/StandardParameterProviderNode.java:
##########
@@ -0,0 +1,609 @@
+/*
+ * 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.controller.parameter;
+
+import org.apache.nifi.annotation.behavior.Restricted;
+import org.apache.nifi.annotation.documentation.DeprecationNotice;
+import org.apache.nifi.authorization.Resource;
+import org.apache.nifi.authorization.resource.Authorizable;
+import org.apache.nifi.authorization.resource.ResourceFactory;
+import org.apache.nifi.authorization.resource.ResourceType;
+import org.apache.nifi.bundle.Bundle;
+import org.apache.nifi.bundle.BundleCoordinate;
+import org.apache.nifi.components.ClassloaderIsolationKeyProvider;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.ConfigurableComponent;
+import org.apache.nifi.components.ValidationContext;
+import org.apache.nifi.components.ValidationResult;
+import org.apache.nifi.components.validation.ValidationStatus;
+import org.apache.nifi.components.validation.ValidationTrigger;
+import org.apache.nifi.controller.AbstractComponentNode;
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.controller.ControllerServiceLookup;
+import org.apache.nifi.controller.LoggableComponent;
+import org.apache.nifi.controller.ParameterProviderNode;
+import org.apache.nifi.controller.ParametersApplication;
+import org.apache.nifi.controller.ReloadComponent;
+import org.apache.nifi.controller.TerminationAwareLogger;
+import org.apache.nifi.controller.ValidationContextFactory;
+import org.apache.nifi.controller.service.ControllerServiceProvider;
+import org.apache.nifi.controller.service.StandardConfigurationContext;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.nar.ExtensionManager;
+import org.apache.nifi.nar.InstanceClassLoader;
+import org.apache.nifi.nar.NarCloseable;
+import org.apache.nifi.parameter.Parameter;
+import org.apache.nifi.parameter.ParameterContext;
+import org.apache.nifi.parameter.ParameterDescriptor;
+import org.apache.nifi.parameter.ParameterGroup;
+import org.apache.nifi.parameter.ParameterGroupConfiguration;
+import org.apache.nifi.parameter.ParameterLookup;
+import org.apache.nifi.parameter.ParameterProvider;
+import org.apache.nifi.parameter.ParameterSensitivity;
+import org.apache.nifi.parameter.VerifiableParameterProvider;
+import org.apache.nifi.registry.ComponentVariableRegistry;
+import org.apache.nifi.util.CharacterFilterUtils;
+import org.apache.nifi.util.FormatUtils;
+import org.apache.nifi.util.file.classloader.ClassLoaderUtils;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.ArrayList;
+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.Objects;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.function.Function;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+public class StandardParameterProviderNode extends AbstractComponentNode 
implements ParameterProviderNode {
+
+    private static final Pattern PARAMETER_NAME_PATTERN = 
Pattern.compile("^[a-zA-Z0-9-_. ]+$");
+
+    private final AtomicReference<ParameterProviderDetails> 
parameterProviderRef;
+    private final ControllerServiceLookup serviceLookup;
+
+    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
+    private final Lock readLock = rwLock.readLock();
+    private final Lock writeLock = rwLock.writeLock();
+
+    private final Set<ParameterContext> referencingParameterContexts;
+
+    private final List<ParameterGroup> fetchedParameterGroups = new 
ArrayList<>();
+
+    private volatile String comment;
+
+    private final Authorizable parentAuthorizable;
+
+    public StandardParameterProviderNode(final 
LoggableComponent<ParameterProvider> parameterProvider, final String id, final 
Authorizable parentAuthorizable,
+                                         final ControllerServiceProvider 
controllerServiceProvider, final ValidationContextFactory 
validationContextFactory,
+                                         final ComponentVariableRegistry 
variableRegistry, final ReloadComponent reloadComponent, final ExtensionManager 
extensionManager,
+                                         final ValidationTrigger 
validationTrigger) {
+
+        this(parameterProvider, id, parentAuthorizable, 
controllerServiceProvider, validationContextFactory,
+                parameterProvider.getComponent().getClass().getSimpleName(), 
parameterProvider.getComponent().getClass().getCanonicalName(),
+                variableRegistry, reloadComponent, extensionManager, 
validationTrigger, false);
+    }
+
+    public StandardParameterProviderNode(final 
LoggableComponent<ParameterProvider> parameterProvider, final String id, final 
Authorizable parentAuthorizable,
+                                         final ControllerServiceProvider 
controllerServiceProvider, final ValidationContextFactory 
validationContextFactory,
+                                         final String componentType, final 
String canonicalClassName, final ComponentVariableRegistry variableRegistry,
+                                         final ReloadComponent 
reloadComponent, final ExtensionManager extensionManager, final 
ValidationTrigger validationTrigger, final boolean isExtensionMissing) {
+        super(id, validationContextFactory, controllerServiceProvider, 
componentType, canonicalClassName, variableRegistry, reloadComponent,
+                extensionManager, validationTrigger, isExtensionMissing);
+        this.parameterProviderRef = new AtomicReference<>(new 
ParameterProviderDetails(parameterProvider));
+        this.serviceLookup = controllerServiceProvider;
+        this.referencingParameterContexts = new HashSet<>();
+        this.parentAuthorizable = parentAuthorizable;
+    }
+
+    @Override
+    public Authorizable getParentAuthorizable() {
+        return parentAuthorizable;
+    }
+
+    @Override
+    public Resource getResource() {
+        return 
ResourceFactory.getComponentResource(ResourceType.ParameterProvider, 
getIdentifier(), getName());
+    }
+
+    @Override
+    public boolean isRestricted() {
+        return 
getParameterProvider().getClass().isAnnotationPresent(Restricted.class);
+    }
+
+    @Override
+    public Class<?> getComponentClass() {
+        return getParameterProvider().getClass();
+    }
+
+    @Override
+    public boolean isDeprecated() {
+        return 
getParameterProvider().getClass().isAnnotationPresent(DeprecationNotice.class);
+    }
+
+    @Override
+    protected ParameterContext getParameterContext() {
+        return null;
+    }
+    @Override
+    public ConfigurableComponent getComponent() {
+        return parameterProviderRef.get().getParameterProvider();
+    }
+
+    @Override
+    public BundleCoordinate getBundleCoordinate() {
+        return parameterProviderRef.get().getBundleCoordinate();
+    }
+
+    @Override
+    public TerminationAwareLogger getLogger() {
+        return parameterProviderRef.get().getComponentLog();
+    }
+
+    @Override
+    public ParameterProvider getParameterProvider() {
+        return parameterProviderRef.get().getParameterProvider();
+    }
+
+    @Override
+    public void setParameterProvider(final 
LoggableComponent<ParameterProvider> parameterProvider) {
+        this.parameterProviderRef.set(new 
ParameterProviderDetails(parameterProvider));
+    }
+
+    @Override
+    public void reload(final Set<URL> additionalUrls) throws 
ParameterProviderInstantiationException {
+        final String additionalResourcesFingerprint = 
ClassLoaderUtils.generateAdditionalUrlsFingerprint(additionalUrls, 
determineClasloaderIsolationKey());
+        setAdditionalResourcesFingerprint(additionalResourcesFingerprint);
+        getReloadComponent().reload(this, getCanonicalClassName(), 
getBundleCoordinate(), additionalUrls);
+    }
+
+    @Override
+    public boolean isValidationNecessary() {
+        return true;
+    }
+
+    @Override
+    public ConfigurationContext getConfigurationContext() {
+        return new StandardConfigurationContext(this, serviceLookup, null, 
getVariableRegistry());
+    }
+
+    @Override
+    public void verifyModifiable() throws IllegalStateException {
+
+    }
+
+    @Override
+    public String getComments() {
+        return comment;
+    }
+
+    @Override
+    public void setComments(final String comment) {
+        this.comment = 
CharacterFilterUtils.filterInvalidXmlCharacters(comment);
+    }
+
+    @Override
+    public void verifyCanClearState() {
+
+    }
+
+    @Override
+    public String toString() {
+        return "ParameterProvider[id=" + getIdentifier() + "]";
+    }
+
+    @Override
+    public String getProcessGroupIdentifier() {
+        return null;
+    }
+
+    @Override
+    public ParameterLookup getParameterLookup() {
+        return ParameterLookup.EMPTY;
+    }
+
+    @Override
+    public Set<ParameterContext> getReferences() {
+        readLock.lock();
+        try {
+            return Collections.unmodifiableSet(referencingParameterContexts);
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    @Override
+    public void addReference(final ParameterContext reference) {
+        writeLock.lock();
+        try {
+            referencingParameterContexts.add(reference);
+        } finally {
+            writeLock.unlock();
+        }
+    }
+
+    @Override
+    public void removeReference(final ParameterContext reference) {
+        writeLock.lock();
+        try {
+            referencingParameterContexts.remove(reference);
+        } finally {
+            writeLock.unlock();
+        }
+    }
+
+    @Override
+    protected List<ValidationResult> validateConfig() {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public void verifyCanFetchParameters() {
+        final ValidationStatus validationStatus = performValidation();
+        if (validationStatus != ValidationStatus.VALID) {
+            throw new IllegalStateException(String.format("Parameter Provider 
[%s] cannot fetch parameters while validation state is %s",
+                    getIdentifier(), validationStatus));
+        }
+    }
+
+    @Override
+    public void fetchParameters() {
+        final ParameterProvider parameterProvider = 
parameterProviderRef.get().getParameterProvider();
+        final ConfigurationContext configurationContext = 
getConfigurationContext();
+        List<ParameterGroup> fetchedParameterGroups;
+        try (final NarCloseable narCloseable = 
NarCloseable.withComponentNarLoader(getExtensionManager(), 
parameterProvider.getClass(), parameterProvider.getIdentifier())) {
+            fetchedParameterGroups = 
parameterProvider.fetchParameters(configurationContext);
+        } catch (final IOException e) {
+            throw new RuntimeException(String.format("Error fetching 
parameters for Parameter Provider [%s]", getName()), e);
+        }
+
+        if (fetchedParameterGroups == null || 
fetchedParameterGroups.isEmpty()) {
+            return;
+        }
+
+        final Set<String> parameterGroupNames = new HashSet<>();
+
+        writeLock.lock();
+        try {
+            this.fetchedParameterGroups.clear();
+            for (final ParameterGroup group : fetchedParameterGroups) {
+                final String groupName = group.getGroupName();
+                if (parameterGroupNames.contains(groupName)) {
+                    throw new IllegalStateException(String.format("Parameter 
group [%s] is provided twice, which is not allowed", groupName));
+                }
+                final Collection<Parameter> parameters = group.getParameters();
+
+                if (parameters == null) {
+                    continue;
+                }
+                final List<Parameter> validParameters = new ArrayList<>();
+                final Set<String> parameterNames = new HashSet<>();
+                for (final Parameter parameter : parameters) {
+                    final ParameterDescriptor descriptor = 
parameter.getDescriptor();
+                    if (descriptor == null) {
+                        throw new IllegalStateException("All fetched 
parameters require a ParameterDescriptor");
+                    }
+                    final String parameterName = descriptor.getName();
+                    if (parameterNames.contains(parameterName)) {
+                        throw new 
IllegalStateException(String.format("Parameter [%s] is provided in group [%s] 
twice, which is not allowed",
+                                parameterName, groupName));
+                    }
+
+                    if (parameter.getValue() == null) {
+                        getLogger().warn("Skipping parameter [{}], which is 
missing a value", new Object[] {parameterName});
+                        continue;
+                    }
+
+                    if 
(PARAMETER_NAME_PATTERN.matcher(parameter.getDescriptor().getName()).matches()) 
{
+                        validParameters.add(parameter);
+                        
parameterNames.add(parameter.getDescriptor().getName());
+                    } else {
+                        getLogger().warn("Skipping parameter [{}}], whose name 
has invalid characters.  Only alpha-numeric characters (a-z, A-Z, 0-9), hyphens 
(-), underscores (_), " +
+                                "periods (.), and spaces ( ) are accepted.", 
new Object[] {parameterName});

Review Comment:
   ```suggestion
                           getLogger().warn("Skipping parameter [{}], whose 
name has invalid characters.  Only alpha-numeric characters (a-z, A-Z, 0-9), 
hyphens (-), underscores (_), " +
                                   "periods (.), and spaces ( ) are accepted.", 
parameterName);
   ```



##########
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/FlowFromDOMFactory.java:
##########
@@ -148,6 +151,21 @@ public static ReportingTaskDTO getReportingTask(final 
Element element, final Pro
         return dto;
     }
 
+    public static ParameterProviderDTO getParameterProvider(final Element 
element, final PropertyEncryptor encryptor, final FlowEncodingVersion 
flowEncodingVersion) {
+        final ParameterProviderDTO dto = new ParameterProviderDTO();
+
+        dto.setId(getString(element, "id"));
+        dto.setName(getString(element, "name"));
+        dto.setComments(getString(element, "comment"));

Review Comment:
   Should these be `comments` instead of `comment`?
   ```suggestion
           dto.setComments(getString(element, "comments"));
   ```



##########
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/parameter/StandardParameterProviderNode.java:
##########
@@ -0,0 +1,609 @@
+/*
+ * 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.controller.parameter;
+
+import org.apache.nifi.annotation.behavior.Restricted;
+import org.apache.nifi.annotation.documentation.DeprecationNotice;
+import org.apache.nifi.authorization.Resource;
+import org.apache.nifi.authorization.resource.Authorizable;
+import org.apache.nifi.authorization.resource.ResourceFactory;
+import org.apache.nifi.authorization.resource.ResourceType;
+import org.apache.nifi.bundle.Bundle;
+import org.apache.nifi.bundle.BundleCoordinate;
+import org.apache.nifi.components.ClassloaderIsolationKeyProvider;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.ConfigurableComponent;
+import org.apache.nifi.components.ValidationContext;
+import org.apache.nifi.components.ValidationResult;
+import org.apache.nifi.components.validation.ValidationStatus;
+import org.apache.nifi.components.validation.ValidationTrigger;
+import org.apache.nifi.controller.AbstractComponentNode;
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.controller.ControllerServiceLookup;
+import org.apache.nifi.controller.LoggableComponent;
+import org.apache.nifi.controller.ParameterProviderNode;
+import org.apache.nifi.controller.ParametersApplication;
+import org.apache.nifi.controller.ReloadComponent;
+import org.apache.nifi.controller.TerminationAwareLogger;
+import org.apache.nifi.controller.ValidationContextFactory;
+import org.apache.nifi.controller.service.ControllerServiceProvider;
+import org.apache.nifi.controller.service.StandardConfigurationContext;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.nar.ExtensionManager;
+import org.apache.nifi.nar.InstanceClassLoader;
+import org.apache.nifi.nar.NarCloseable;
+import org.apache.nifi.parameter.Parameter;
+import org.apache.nifi.parameter.ParameterContext;
+import org.apache.nifi.parameter.ParameterDescriptor;
+import org.apache.nifi.parameter.ParameterGroup;
+import org.apache.nifi.parameter.ParameterGroupConfiguration;
+import org.apache.nifi.parameter.ParameterLookup;
+import org.apache.nifi.parameter.ParameterProvider;
+import org.apache.nifi.parameter.ParameterSensitivity;
+import org.apache.nifi.parameter.VerifiableParameterProvider;
+import org.apache.nifi.registry.ComponentVariableRegistry;
+import org.apache.nifi.util.CharacterFilterUtils;
+import org.apache.nifi.util.FormatUtils;
+import org.apache.nifi.util.file.classloader.ClassLoaderUtils;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.ArrayList;
+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.Objects;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.function.Function;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+public class StandardParameterProviderNode extends AbstractComponentNode 
implements ParameterProviderNode {
+
+    private static final Pattern PARAMETER_NAME_PATTERN = 
Pattern.compile("^[a-zA-Z0-9-_. ]+$");
+
+    private final AtomicReference<ParameterProviderDetails> 
parameterProviderRef;
+    private final ControllerServiceLookup serviceLookup;
+
+    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
+    private final Lock readLock = rwLock.readLock();
+    private final Lock writeLock = rwLock.writeLock();
+
+    private final Set<ParameterContext> referencingParameterContexts;
+
+    private final List<ParameterGroup> fetchedParameterGroups = new 
ArrayList<>();
+
+    private volatile String comment;
+
+    private final Authorizable parentAuthorizable;
+
+    public StandardParameterProviderNode(final 
LoggableComponent<ParameterProvider> parameterProvider, final String id, final 
Authorizable parentAuthorizable,
+                                         final ControllerServiceProvider 
controllerServiceProvider, final ValidationContextFactory 
validationContextFactory,
+                                         final ComponentVariableRegistry 
variableRegistry, final ReloadComponent reloadComponent, final ExtensionManager 
extensionManager,
+                                         final ValidationTrigger 
validationTrigger) {
+
+        this(parameterProvider, id, parentAuthorizable, 
controllerServiceProvider, validationContextFactory,
+                parameterProvider.getComponent().getClass().getSimpleName(), 
parameterProvider.getComponent().getClass().getCanonicalName(),
+                variableRegistry, reloadComponent, extensionManager, 
validationTrigger, false);
+    }
+
+    public StandardParameterProviderNode(final 
LoggableComponent<ParameterProvider> parameterProvider, final String id, final 
Authorizable parentAuthorizable,
+                                         final ControllerServiceProvider 
controllerServiceProvider, final ValidationContextFactory 
validationContextFactory,
+                                         final String componentType, final 
String canonicalClassName, final ComponentVariableRegistry variableRegistry,
+                                         final ReloadComponent 
reloadComponent, final ExtensionManager extensionManager, final 
ValidationTrigger validationTrigger, final boolean isExtensionMissing) {
+        super(id, validationContextFactory, controllerServiceProvider, 
componentType, canonicalClassName, variableRegistry, reloadComponent,
+                extensionManager, validationTrigger, isExtensionMissing);
+        this.parameterProviderRef = new AtomicReference<>(new 
ParameterProviderDetails(parameterProvider));
+        this.serviceLookup = controllerServiceProvider;
+        this.referencingParameterContexts = new HashSet<>();
+        this.parentAuthorizable = parentAuthorizable;
+    }
+
+    @Override
+    public Authorizable getParentAuthorizable() {
+        return parentAuthorizable;
+    }
+
+    @Override
+    public Resource getResource() {
+        return 
ResourceFactory.getComponentResource(ResourceType.ParameterProvider, 
getIdentifier(), getName());
+    }
+
+    @Override
+    public boolean isRestricted() {
+        return 
getParameterProvider().getClass().isAnnotationPresent(Restricted.class);
+    }
+
+    @Override
+    public Class<?> getComponentClass() {
+        return getParameterProvider().getClass();
+    }
+
+    @Override
+    public boolean isDeprecated() {
+        return 
getParameterProvider().getClass().isAnnotationPresent(DeprecationNotice.class);
+    }
+
+    @Override
+    protected ParameterContext getParameterContext() {
+        return null;
+    }
+    @Override
+    public ConfigurableComponent getComponent() {
+        return parameterProviderRef.get().getParameterProvider();
+    }
+
+    @Override
+    public BundleCoordinate getBundleCoordinate() {
+        return parameterProviderRef.get().getBundleCoordinate();
+    }
+
+    @Override
+    public TerminationAwareLogger getLogger() {
+        return parameterProviderRef.get().getComponentLog();
+    }
+
+    @Override
+    public ParameterProvider getParameterProvider() {
+        return parameterProviderRef.get().getParameterProvider();
+    }
+
+    @Override
+    public void setParameterProvider(final 
LoggableComponent<ParameterProvider> parameterProvider) {
+        this.parameterProviderRef.set(new 
ParameterProviderDetails(parameterProvider));
+    }
+
+    @Override
+    public void reload(final Set<URL> additionalUrls) throws 
ParameterProviderInstantiationException {
+        final String additionalResourcesFingerprint = 
ClassLoaderUtils.generateAdditionalUrlsFingerprint(additionalUrls, 
determineClasloaderIsolationKey());
+        setAdditionalResourcesFingerprint(additionalResourcesFingerprint);
+        getReloadComponent().reload(this, getCanonicalClassName(), 
getBundleCoordinate(), additionalUrls);
+    }
+
+    @Override
+    public boolean isValidationNecessary() {
+        return true;
+    }
+
+    @Override
+    public ConfigurationContext getConfigurationContext() {
+        return new StandardConfigurationContext(this, serviceLookup, null, 
getVariableRegistry());
+    }
+
+    @Override
+    public void verifyModifiable() throws IllegalStateException {
+
+    }
+
+    @Override
+    public String getComments() {
+        return comment;
+    }
+
+    @Override
+    public void setComments(final String comment) {
+        this.comment = 
CharacterFilterUtils.filterInvalidXmlCharacters(comment);
+    }
+
+    @Override
+    public void verifyCanClearState() {
+
+    }
+
+    @Override
+    public String toString() {
+        return "ParameterProvider[id=" + getIdentifier() + "]";
+    }
+
+    @Override
+    public String getProcessGroupIdentifier() {
+        return null;
+    }
+
+    @Override
+    public ParameterLookup getParameterLookup() {
+        return ParameterLookup.EMPTY;
+    }
+
+    @Override
+    public Set<ParameterContext> getReferences() {
+        readLock.lock();
+        try {
+            return Collections.unmodifiableSet(referencingParameterContexts);
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    @Override
+    public void addReference(final ParameterContext reference) {
+        writeLock.lock();
+        try {
+            referencingParameterContexts.add(reference);
+        } finally {
+            writeLock.unlock();
+        }
+    }
+
+    @Override
+    public void removeReference(final ParameterContext reference) {
+        writeLock.lock();
+        try {
+            referencingParameterContexts.remove(reference);
+        } finally {
+            writeLock.unlock();
+        }
+    }
+
+    @Override
+    protected List<ValidationResult> validateConfig() {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public void verifyCanFetchParameters() {
+        final ValidationStatus validationStatus = performValidation();
+        if (validationStatus != ValidationStatus.VALID) {
+            throw new IllegalStateException(String.format("Parameter Provider 
[%s] cannot fetch parameters while validation state is %s",
+                    getIdentifier(), validationStatus));
+        }
+    }
+
+    @Override
+    public void fetchParameters() {
+        final ParameterProvider parameterProvider = 
parameterProviderRef.get().getParameterProvider();
+        final ConfigurationContext configurationContext = 
getConfigurationContext();
+        List<ParameterGroup> fetchedParameterGroups;
+        try (final NarCloseable narCloseable = 
NarCloseable.withComponentNarLoader(getExtensionManager(), 
parameterProvider.getClass(), parameterProvider.getIdentifier())) {
+            fetchedParameterGroups = 
parameterProvider.fetchParameters(configurationContext);
+        } catch (final IOException e) {
+            throw new RuntimeException(String.format("Error fetching 
parameters for Parameter Provider [%s]", getName()), e);
+        }
+
+        if (fetchedParameterGroups == null || 
fetchedParameterGroups.isEmpty()) {
+            return;
+        }
+
+        final Set<String> parameterGroupNames = new HashSet<>();
+
+        writeLock.lock();
+        try {
+            this.fetchedParameterGroups.clear();
+            for (final ParameterGroup group : fetchedParameterGroups) {
+                final String groupName = group.getGroupName();
+                if (parameterGroupNames.contains(groupName)) {
+                    throw new IllegalStateException(String.format("Parameter 
group [%s] is provided twice, which is not allowed", groupName));
+                }
+                final Collection<Parameter> parameters = group.getParameters();
+
+                if (parameters == null) {
+                    continue;
+                }
+                final List<Parameter> validParameters = new ArrayList<>();
+                final Set<String> parameterNames = new HashSet<>();
+                for (final Parameter parameter : parameters) {
+                    final ParameterDescriptor descriptor = 
parameter.getDescriptor();
+                    if (descriptor == null) {
+                        throw new IllegalStateException("All fetched 
parameters require a ParameterDescriptor");
+                    }
+                    final String parameterName = descriptor.getName();
+                    if (parameterNames.contains(parameterName)) {
+                        throw new 
IllegalStateException(String.format("Parameter [%s] is provided in group [%s] 
twice, which is not allowed",
+                                parameterName, groupName));
+                    }
+
+                    if (parameter.getValue() == null) {
+                        getLogger().warn("Skipping parameter [{}], which is 
missing a value", new Object[] {parameterName});
+                        continue;
+                    }
+
+                    if 
(PARAMETER_NAME_PATTERN.matcher(parameter.getDescriptor().getName()).matches()) 
{
+                        validParameters.add(parameter);
+                        
parameterNames.add(parameter.getDescriptor().getName());
+                    } else {
+                        getLogger().warn("Skipping parameter [{}}], whose name 
has invalid characters.  Only alpha-numeric characters (a-z, A-Z, 0-9), hyphens 
(-), underscores (_), " +
+                                "periods (.), and spaces ( ) are accepted.", 
new Object[] {parameterName});
+                    }
+                }
+                this.fetchedParameterGroups.add(new ParameterGroup(groupName, 
toProvidedParameters(validParameters)));
+                parameterGroupNames.add(groupName);
+            }
+        } finally {
+            writeLock.unlock();
+        }
+    }
+
+    @Override
+    public void verifyCanApplyParameters(final 
Collection<ParameterGroupConfiguration> parameterGroupConfigurations) {
+        if (fetchedParameterGroups.isEmpty()) {
+            return;
+        }
+        readLock.lock();
+        try {
+            final Map<String, ParameterGroupConfiguration> 
parameterGroupConfigurationMap = parameterGroupConfigurations.stream()
+                    
.collect(Collectors.toMap(ParameterGroupConfiguration::getParameterContextName, 
Function.identity()));
+            for (final ParameterContext reference : getReferences()) {
+                final ParameterGroupConfiguration groupConfiguration = 
parameterGroupConfigurationMap.get(reference.getName());
+                if (groupConfiguration == null) {
+                    continue;
+                }
+                
reference.verifyCanSetParameters(getFetchedParameterUpdateMap(reference, 
groupConfiguration));
+            }
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    @Override
+    public void verifyCanDelete() {
+        if (!getReferences().isEmpty()) {
+            final String contextNameList = getReferences().stream()
+                    .map(ParameterContext::getName)
+                    .collect(Collectors.joining(", "));
+            throw new IllegalStateException(String.format("Cannot delete 
Parameter Provider [%s] while it is referenced by Contexts: [%s]", 
getIdentifier(), contextNameList));
+        }
+    }
+
+    @Override
+    public List<ConfigVerificationResult> verifyConfiguration(final 
ConfigurationContext context, final ComponentLog logger, final ExtensionManager 
extensionManager) {
+        final List<ConfigVerificationResult> results = new ArrayList<>();
+
+        try {
+            final long startNanos = System.nanoTime();
+            // Call super's verifyConfig, which will perform component 
validation
+            results.addAll(super.verifyConfig(context.getProperties(), 
context.getAnnotationData(), null));
+            final long validationComplete = System.nanoTime();
+
+            // If any invalid outcomes from validation, we do not want to 
perform additional verification, because we only run additional verification 
when the component is valid.
+            // This is done in order to make it much simpler to develop these 
verifications, since the developer doesn't have to worry about whether or not 
the given values are valid.
+            if (!results.isEmpty() && results.stream().anyMatch(result -> 
result.getOutcome() == Outcome.FAILED)) {
+                return results;
+            }
+
+            final ParameterProvider parameterProvider = getParameterProvider();
+            if (parameterProvider instanceof VerifiableParameterProvider) {
+                logger.debug("{} is a VerifiableParameterProvider. Will 
perform full verification of configuration.", this);
+                final VerifiableParameterProvider verifiable = 
(VerifiableParameterProvider) parameterProvider;
+
+                // Check if the given configuration requires a different 
classloader than the current configuration
+                final boolean classpathDifferent = 
isClasspathDifferent(context.getProperties());
+
+                if (classpathDifferent) {
+                    // Create a classloader for the given configuration and 
use that to verify the component's configuration
+                    final Bundle bundle = 
extensionManager.getBundle(getBundleCoordinate());
+                    final Set<URL> classpathUrls = 
getAdditionalClasspathResources(context.getProperties().keySet(), descriptor -> 
context.getProperty(descriptor).getValue());
+
+                    final String classloaderIsolationKey = 
getClassLoaderIsolationKey(context);
+
+                    final ClassLoader currentClassLoader = 
Thread.currentThread().getContextClassLoader();
+                    try (final InstanceClassLoader detectedClassLoader = 
extensionManager.createInstanceClassLoader(getComponentType(), getIdentifier(), 
bundle, classpathUrls, false,
+                            classloaderIsolationKey)) {
+                        
Thread.currentThread().setContextClassLoader(detectedClassLoader);
+                        results.addAll(verifiable.verify(context, logger));
+                    } finally {
+                        
Thread.currentThread().setContextClassLoader(currentClassLoader);
+                    }
+                } else {
+                    // Verify the configuration, using the component's 
classloader
+                    try (final NarCloseable narCloseable = 
NarCloseable.withComponentNarLoader(extensionManager, 
parameterProvider.getClass(), getIdentifier())) {
+                        results.addAll(verifiable.verify(context, logger));
+                    }
+                }
+
+                final long validationNanos = validationComplete - startNanos;
+                final long verificationNanos = System.nanoTime() - 
validationComplete;
+                logger.debug("{} completed full configuration validation in {} 
plus {} for validation",
+                        this, FormatUtils.formatNanos(verificationNanos, 
false), FormatUtils.formatNanos(validationNanos, false));
+            } else {
+                logger.debug("{} is not a VerifiableParameterProvider, so will 
not perform full verification of configuration. Validation took {}", this,
+                        FormatUtils.formatNanos(validationComplete - 
startNanos, false));
+            }
+        } catch (final Throwable t) {
+            logger.error("Failed to perform verification of Parameter 
Provider's configuration for {}", this, t);
+
+            results.add(new ConfigVerificationResult.Builder()
+                    .outcome(Outcome.FAILED)
+                    .verificationStepName("Perform Verification")
+                    .explanation("Encountered unexpected failure when 
attempting to perform verification: " + t)
+                    .build());
+        }
+
+        return results;
+    }
+
+    /**
+     * Using the existing parameters from the ParameterContext and the fetched 
parameters in the provider, constructs
+     * a map from Parameter name to updated Parameter (or null if the 
parameter has been removed in the fetch).
+     * @param parameterContext A ParameterContext
+     * @param parameterGroupConfiguration The configuration for the fetched 
parameter group
+     * @return A map from name to Parameter (or null if parameter should be 
removed)
+     */
+    private Map<String, Parameter> getFetchedParameterUpdateMap(final 
ParameterContext parameterContext, ParameterGroupConfiguration 
parameterGroupConfiguration) {
+        final Map<String, Parameter> parameterUpdateMap = new HashMap<>();
+
+        final ParameterGroup parameterGroup = fetchedParameterGroups.stream()
+                .filter(group -> 
parameterContext.getParameterProviderConfiguration().getParameterGroupName().equals(group.getGroupName()))
+                .findFirst()
+                .orElse(null);
+
+        if (parameterGroup == null) {
+            return parameterUpdateMap;
+        }
+
+        // Get a filtered list of the parameters with their sensitivity set 
based on group configuration
+        final List<Parameter> configuredParameters = 
configureParameters(parameterGroup.getParameters(), 
parameterGroupConfiguration);
+
+        final Map<ParameterDescriptor, Parameter> fetchedParameterMap = 
configuredParameters.stream()
+                .collect(Collectors.toMap(Parameter::getDescriptor, 
Function.identity()));
+
+        final Map<ParameterDescriptor, Parameter> currentParameters = 
parameterContext.getParameters();
+        // Find parameters that were removed
+        currentParameters.keySet().forEach(descriptor -> {
+                if (!fetchedParameterMap.containsKey(descriptor)) {
+                    parameterUpdateMap.put(descriptor.getName(), null);
+                }
+        });
+        // Add all changed and new parameters
+        for (final Map.Entry<ParameterDescriptor, Parameter> entry : 
fetchedParameterMap.entrySet()) {
+            final ParameterDescriptor descriptor = entry.getKey();
+            final Parameter fetchedParameter = entry.getValue();
+            final Parameter currentParameter = 
currentParameters.get(descriptor);
+            if (currentParameter == null) {
+                // Add if it's a new parameter
+                parameterUpdateMap.put(descriptor.getName(), fetchedParameter);
+            } else {
+                final boolean isSensitivityChanged = 
currentParameter.getDescriptor().isSensitive() != 
fetchedParameter.getDescriptor().isSensitive();
+                if (!Objects.equals(currentParameter.getValue(), 
fetchedParameter.getValue()) || isSensitivityChanged) {
+                    // Also add if it's an existing parameter that has a 
changed value
+                    parameterUpdateMap.put(descriptor.getName(), 
fetchedParameter);
+
+                    if (isSensitivityChanged) {
+                        final ParameterSensitivity currentSensitivity = 
currentParameter.getDescriptor().isSensitive() ? ParameterSensitivity.SENSITIVE 
: ParameterSensitivity.NON_SENSITIVE;
+                        final ParameterSensitivity fetchedSensitivity = 
fetchedParameter.getDescriptor().isSensitive() ? ParameterSensitivity.SENSITIVE 
: ParameterSensitivity.NON_SENSITIVE;
+                        getLogger().info("Parameter [{}] is being changed from 
{} to {}", new Object[] { descriptor.getName(),
+                                currentSensitivity.getName(), 
fetchedSensitivity.getName()});

Review Comment:
   ```suggestion
                           getLogger().info("Parameter [{}] sensitivity is 
being changed from {} to {}", descriptor.getName(),
                                   currentSensitivity.getName(), 
fetchedSensitivity.getName());
   ```



##########
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/StandardFlowSerializer.java:
##########
@@ -656,6 +673,20 @@ public static void addReportingTask(final Element element, 
final ReportingTaskNo
         element.appendChild(taskElement);
     }
 
+    public static void addParameterProvider(final Element element, final 
ParameterProviderNode providerNode, final PropertyEncryptor encryptor) {
+        final Element taskElement = 
element.getOwnerDocument().createElement("parameterProvider");
+        addTextElement(taskElement, "id", providerNode.getIdentifier());
+        addTextElement(taskElement, "name", providerNode.getName());
+        addTextElement(taskElement, "comment", providerNode.getComments());

Review Comment:
   ```suggestion
           addTextElement(taskElement, "comments", providerNode.getComments());
   ```



##########
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/FlowConfiguration.xsd:
##########
@@ -69,6 +71,9 @@
             <xs:element name="name" type="NonEmptyStringType" />
             <xs:element name="description" type="xs:string" minOccurs="0" 
maxOccurs="1" />
             <xs:element name="inheritedParameterContextId" type="xs:string" 
minOccurs="0" maxOccurs="unbounded" />
+            <xs:element name="parameterProviderId" type="xs:string" 
minOccurs="0" maxOccurs="1" />
+            <xs:element name="parameterGroupName" type="xs:string" 
minOccurs="0" maxOccurs="1" />
+            <xs:element name="isSynchronized" type="xs:boolean" minOccurs="0" 
maxOccurs="1" />

Review Comment:
   ```suggestion
               <xs:element name="synchronized" type="xs:boolean" minOccurs="0" 
maxOccurs="1" />
   ```



##########
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/resources/META-INF/services/org.apache.nifi.parameter.ParameterProvider:
##########
@@ -0,0 +1,15 @@
+# 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.
+org.apache.nifi.parameter.mock.DummyParameterProvider

Review Comment:
   Noting this for adjustment along with the class name.



##########
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ParameterProviderDTO.java:
##########
@@ -0,0 +1,301 @@
+/*
+ * 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;
+
+import io.swagger.annotations.ApiModelProperty;
+import org.apache.nifi.web.api.entity.AffectedComponentEntity;
+import 
org.apache.nifi.web.api.entity.ParameterProviderReferencingComponentEntity;
+import org.apache.nifi.web.api.entity.ParameterGroupConfigurationEntity;
+
+import javax.xml.bind.annotation.XmlType;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Component that is capable of providing parameters to NiFi from an external 
source
+ */
+@XmlType(name = "parameterProvider")
+public class ParameterProviderDTO extends ComponentDTO {
+    public static final String VALID = "VALID";
+    public static final String INVALID = "INVALID";
+    public static final String VALIDATING = "VALIDATING";
+
+    private String name;
+    private String type;
+    private BundleDTO bundle;
+    private String comments;
+    private Boolean persistsState;
+    private Boolean restricted;
+    private Boolean deprecated;
+    private Boolean isExtensionMissing;

Review Comment:
   ```suggestion
       private Boolean extensionMissing;
   ```



##########
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/parameter/StandardParameterProviderNode.java:
##########
@@ -0,0 +1,609 @@
+/*
+ * 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.controller.parameter;
+
+import org.apache.nifi.annotation.behavior.Restricted;
+import org.apache.nifi.annotation.documentation.DeprecationNotice;
+import org.apache.nifi.authorization.Resource;
+import org.apache.nifi.authorization.resource.Authorizable;
+import org.apache.nifi.authorization.resource.ResourceFactory;
+import org.apache.nifi.authorization.resource.ResourceType;
+import org.apache.nifi.bundle.Bundle;
+import org.apache.nifi.bundle.BundleCoordinate;
+import org.apache.nifi.components.ClassloaderIsolationKeyProvider;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.ConfigurableComponent;
+import org.apache.nifi.components.ValidationContext;
+import org.apache.nifi.components.ValidationResult;
+import org.apache.nifi.components.validation.ValidationStatus;
+import org.apache.nifi.components.validation.ValidationTrigger;
+import org.apache.nifi.controller.AbstractComponentNode;
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.controller.ControllerServiceLookup;
+import org.apache.nifi.controller.LoggableComponent;
+import org.apache.nifi.controller.ParameterProviderNode;
+import org.apache.nifi.controller.ParametersApplication;
+import org.apache.nifi.controller.ReloadComponent;
+import org.apache.nifi.controller.TerminationAwareLogger;
+import org.apache.nifi.controller.ValidationContextFactory;
+import org.apache.nifi.controller.service.ControllerServiceProvider;
+import org.apache.nifi.controller.service.StandardConfigurationContext;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.nar.ExtensionManager;
+import org.apache.nifi.nar.InstanceClassLoader;
+import org.apache.nifi.nar.NarCloseable;
+import org.apache.nifi.parameter.Parameter;
+import org.apache.nifi.parameter.ParameterContext;
+import org.apache.nifi.parameter.ParameterDescriptor;
+import org.apache.nifi.parameter.ParameterGroup;
+import org.apache.nifi.parameter.ParameterGroupConfiguration;
+import org.apache.nifi.parameter.ParameterLookup;
+import org.apache.nifi.parameter.ParameterProvider;
+import org.apache.nifi.parameter.ParameterSensitivity;
+import org.apache.nifi.parameter.VerifiableParameterProvider;
+import org.apache.nifi.registry.ComponentVariableRegistry;
+import org.apache.nifi.util.CharacterFilterUtils;
+import org.apache.nifi.util.FormatUtils;
+import org.apache.nifi.util.file.classloader.ClassLoaderUtils;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.ArrayList;
+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.Objects;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.function.Function;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+public class StandardParameterProviderNode extends AbstractComponentNode 
implements ParameterProviderNode {
+
+    private static final Pattern PARAMETER_NAME_PATTERN = 
Pattern.compile("^[a-zA-Z0-9-_. ]+$");
+
+    private final AtomicReference<ParameterProviderDetails> 
parameterProviderRef;
+    private final ControllerServiceLookup serviceLookup;
+
+    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
+    private final Lock readLock = rwLock.readLock();
+    private final Lock writeLock = rwLock.writeLock();
+
+    private final Set<ParameterContext> referencingParameterContexts;
+
+    private final List<ParameterGroup> fetchedParameterGroups = new 
ArrayList<>();
+
+    private volatile String comment;
+
+    private final Authorizable parentAuthorizable;
+
+    public StandardParameterProviderNode(final 
LoggableComponent<ParameterProvider> parameterProvider, final String id, final 
Authorizable parentAuthorizable,
+                                         final ControllerServiceProvider 
controllerServiceProvider, final ValidationContextFactory 
validationContextFactory,
+                                         final ComponentVariableRegistry 
variableRegistry, final ReloadComponent reloadComponent, final ExtensionManager 
extensionManager,
+                                         final ValidationTrigger 
validationTrigger) {
+
+        this(parameterProvider, id, parentAuthorizable, 
controllerServiceProvider, validationContextFactory,
+                parameterProvider.getComponent().getClass().getSimpleName(), 
parameterProvider.getComponent().getClass().getCanonicalName(),
+                variableRegistry, reloadComponent, extensionManager, 
validationTrigger, false);
+    }
+
+    public StandardParameterProviderNode(final 
LoggableComponent<ParameterProvider> parameterProvider, final String id, final 
Authorizable parentAuthorizable,
+                                         final ControllerServiceProvider 
controllerServiceProvider, final ValidationContextFactory 
validationContextFactory,
+                                         final String componentType, final 
String canonicalClassName, final ComponentVariableRegistry variableRegistry,
+                                         final ReloadComponent 
reloadComponent, final ExtensionManager extensionManager, final 
ValidationTrigger validationTrigger, final boolean isExtensionMissing) {
+        super(id, validationContextFactory, controllerServiceProvider, 
componentType, canonicalClassName, variableRegistry, reloadComponent,
+                extensionManager, validationTrigger, isExtensionMissing);
+        this.parameterProviderRef = new AtomicReference<>(new 
ParameterProviderDetails(parameterProvider));
+        this.serviceLookup = controllerServiceProvider;
+        this.referencingParameterContexts = new HashSet<>();
+        this.parentAuthorizable = parentAuthorizable;
+    }
+
+    @Override
+    public Authorizable getParentAuthorizable() {
+        return parentAuthorizable;
+    }
+
+    @Override
+    public Resource getResource() {
+        return 
ResourceFactory.getComponentResource(ResourceType.ParameterProvider, 
getIdentifier(), getName());
+    }
+
+    @Override
+    public boolean isRestricted() {
+        return 
getParameterProvider().getClass().isAnnotationPresent(Restricted.class);
+    }
+
+    @Override
+    public Class<?> getComponentClass() {
+        return getParameterProvider().getClass();
+    }
+
+    @Override
+    public boolean isDeprecated() {
+        return 
getParameterProvider().getClass().isAnnotationPresent(DeprecationNotice.class);
+    }
+
+    @Override
+    protected ParameterContext getParameterContext() {
+        return null;
+    }
+    @Override
+    public ConfigurableComponent getComponent() {
+        return parameterProviderRef.get().getParameterProvider();
+    }
+
+    @Override
+    public BundleCoordinate getBundleCoordinate() {
+        return parameterProviderRef.get().getBundleCoordinate();
+    }
+
+    @Override
+    public TerminationAwareLogger getLogger() {
+        return parameterProviderRef.get().getComponentLog();
+    }
+
+    @Override
+    public ParameterProvider getParameterProvider() {
+        return parameterProviderRef.get().getParameterProvider();
+    }
+
+    @Override
+    public void setParameterProvider(final 
LoggableComponent<ParameterProvider> parameterProvider) {
+        this.parameterProviderRef.set(new 
ParameterProviderDetails(parameterProvider));
+    }
+
+    @Override
+    public void reload(final Set<URL> additionalUrls) throws 
ParameterProviderInstantiationException {
+        final String additionalResourcesFingerprint = 
ClassLoaderUtils.generateAdditionalUrlsFingerprint(additionalUrls, 
determineClasloaderIsolationKey());
+        setAdditionalResourcesFingerprint(additionalResourcesFingerprint);
+        getReloadComponent().reload(this, getCanonicalClassName(), 
getBundleCoordinate(), additionalUrls);
+    }
+
+    @Override
+    public boolean isValidationNecessary() {
+        return true;
+    }
+
+    @Override
+    public ConfigurationContext getConfigurationContext() {
+        return new StandardConfigurationContext(this, serviceLookup, null, 
getVariableRegistry());
+    }
+
+    @Override
+    public void verifyModifiable() throws IllegalStateException {
+
+    }
+
+    @Override
+    public String getComments() {
+        return comment;
+    }
+
+    @Override
+    public void setComments(final String comment) {
+        this.comment = 
CharacterFilterUtils.filterInvalidXmlCharacters(comment);
+    }
+
+    @Override
+    public void verifyCanClearState() {
+
+    }
+
+    @Override
+    public String toString() {
+        return "ParameterProvider[id=" + getIdentifier() + "]";
+    }
+
+    @Override
+    public String getProcessGroupIdentifier() {
+        return null;
+    }
+
+    @Override
+    public ParameterLookup getParameterLookup() {
+        return ParameterLookup.EMPTY;
+    }
+
+    @Override
+    public Set<ParameterContext> getReferences() {
+        readLock.lock();
+        try {
+            return Collections.unmodifiableSet(referencingParameterContexts);
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    @Override
+    public void addReference(final ParameterContext reference) {
+        writeLock.lock();
+        try {
+            referencingParameterContexts.add(reference);
+        } finally {
+            writeLock.unlock();
+        }
+    }
+
+    @Override
+    public void removeReference(final ParameterContext reference) {
+        writeLock.lock();
+        try {
+            referencingParameterContexts.remove(reference);
+        } finally {
+            writeLock.unlock();
+        }
+    }
+
+    @Override
+    protected List<ValidationResult> validateConfig() {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public void verifyCanFetchParameters() {
+        final ValidationStatus validationStatus = performValidation();
+        if (validationStatus != ValidationStatus.VALID) {
+            throw new IllegalStateException(String.format("Parameter Provider 
[%s] cannot fetch parameters while validation state is %s",
+                    getIdentifier(), validationStatus));
+        }
+    }
+
+    @Override
+    public void fetchParameters() {
+        final ParameterProvider parameterProvider = 
parameterProviderRef.get().getParameterProvider();
+        final ConfigurationContext configurationContext = 
getConfigurationContext();
+        List<ParameterGroup> fetchedParameterGroups;
+        try (final NarCloseable narCloseable = 
NarCloseable.withComponentNarLoader(getExtensionManager(), 
parameterProvider.getClass(), parameterProvider.getIdentifier())) {
+            fetchedParameterGroups = 
parameterProvider.fetchParameters(configurationContext);
+        } catch (final IOException e) {
+            throw new RuntimeException(String.format("Error fetching 
parameters for Parameter Provider [%s]", getName()), e);
+        }
+
+        if (fetchedParameterGroups == null || 
fetchedParameterGroups.isEmpty()) {
+            return;
+        }
+
+        final Set<String> parameterGroupNames = new HashSet<>();
+
+        writeLock.lock();
+        try {
+            this.fetchedParameterGroups.clear();
+            for (final ParameterGroup group : fetchedParameterGroups) {
+                final String groupName = group.getGroupName();
+                if (parameterGroupNames.contains(groupName)) {
+                    throw new IllegalStateException(String.format("Parameter 
group [%s] is provided twice, which is not allowed", groupName));
+                }
+                final Collection<Parameter> parameters = group.getParameters();
+
+                if (parameters == null) {
+                    continue;
+                }
+                final List<Parameter> validParameters = new ArrayList<>();
+                final Set<String> parameterNames = new HashSet<>();
+                for (final Parameter parameter : parameters) {
+                    final ParameterDescriptor descriptor = 
parameter.getDescriptor();
+                    if (descriptor == null) {
+                        throw new IllegalStateException("All fetched 
parameters require a ParameterDescriptor");
+                    }
+                    final String parameterName = descriptor.getName();
+                    if (parameterNames.contains(parameterName)) {
+                        throw new 
IllegalStateException(String.format("Parameter [%s] is provided in group [%s] 
twice, which is not allowed",
+                                parameterName, groupName));
+                    }
+
+                    if (parameter.getValue() == null) {
+                        getLogger().warn("Skipping parameter [{}], which is 
missing a value", new Object[] {parameterName});

Review Comment:
   ```suggestion
                           getLogger().warn("Skipping parameter [{}], which is 
missing a value", parameterName);
   ```



##########
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/AbstractComponentNode.java:
##########
@@ -1129,17 +1129,21 @@ public Optional<Parameter> getParameter(final String 
parameterName) {
 
                 // There is an update to the parameter. We want to return the 
previous value of the Parameter.
                 final ParameterDescriptor parameterDescriptor;
+                final boolean isProvided;

Review Comment:
   ```suggestion
                   final boolean provided;
   ```



##########
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/FlowConfiguration.xsd:
##########
@@ -534,6 +540,25 @@
         </xs:sequence>
     </xs:complexType>
 
+    <xs:complexType name="ParameterProvidersType">
+        <xs:sequence>
+            <xs:element name="parameterProvider" type="ParameterProviderType" 
minOccurs="0" maxOccurs="unbounded" />
+        </xs:sequence>
+    </xs:complexType>
+
+    <xs:complexType name="ParameterProviderType">
+        <xs:sequence>
+            <xs:element name="id" type="NonEmptyStringType" />
+            <xs:element name="name" type="NonEmptyStringType" />
+            <xs:element name="comment" type="xs:string" />

Review Comment:
   ```suggestion
               <xs:element name="comments" type="xs:string" />
   ```



##########
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardReloadComponent.java:
##########
@@ -209,4 +211,52 @@ public void reload(final ReportingTaskNode existingNode, 
final String newType, f
         flowController.getValidationTrigger().triggerAsync(existingNode);
     }
 
+    @Override
+    public void reload(final ParameterProviderNode existingNode, final String 
newType, final BundleCoordinate bundleCoordinate, final Set<URL> additionalUrls)
+            throws ParameterProviderInstantiationException {
+        if (existingNode == null) {
+            throw new IllegalStateException("Existing ParameterProviderNode 
cannot be null");
+        }
+
+        final String id = existingNode.getParameterProvider().getIdentifier();
+
+        // ghost components will have a null logger
+        if (existingNode.getLogger() != null) {
+            existingNode.getLogger().debug("Reloading component {} to type {} 
from bundle {}", new Object[]{id, newType, bundleCoordinate});

Review Comment:
   ```suggestion
               existingNode.getLogger().debug("Reloading component {} to type 
{} from bundle {}", id, newType, bundleCoordinate);
   ```



##########
nifi-nar-bundles/nifi-standard-bundle/nifi-standard-parameter-providers/src/main/java/org/apache/nifi/parameter/FileParameterProvider.java:
##########
@@ -0,0 +1,242 @@
+/*
+ * 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.parameter;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.nifi.annotation.behavior.Restricted;
+import org.apache.nifi.annotation.behavior.Restriction;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.components.AllowableValue;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.RequiredPermission;
+import org.apache.nifi.components.ValidationContext;
+import org.apache.nifi.components.ValidationResult;
+import org.apache.nifi.components.Validator;
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.DataUnit;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.stream.io.LimitingInputStream;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+@Tags({"file"})
+@CapabilityDescription("Fetches parameters from files.  Parameter groups are 
indicated by a set of directories, and files within the directories map to 
parameter names. " +
+        "The content of the file becomes the parameter value.")
+
+@Restricted(
+        restrictions = {
+                @Restriction(
+                        requiredPermission = 
RequiredPermission.READ_FILESYSTEM,
+                        explanation = "Provides operator the ability to read 
from any file that NiFi has access to.")
+        }
+)
+public class FileParameterProvider extends AbstractParameterProvider 
implements VerifiableParameterProvider  {
+    private static final int MAX_SIZE_LIMIT = 8096;
+    private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
+
+    private enum ParameterValueEncoding {
+        BASE64,
+        PLAINTEXT
+    }
+
+    private static final AllowableValue BASE64_ENCODING = new 
AllowableValue("base64", "Base64", "File content is Base64-encoded, " +
+            "and will be decoded before providing the value as a Parameter.");
+    private static final AllowableValue PLAIN_TEXT = new 
AllowableValue("plaintext", "Plain text", "File content is not encoded, " +
+            "and will be provided directly as a Parameter value.");
+
+    public static final PropertyDescriptor PARAMETER_GROUP_DIRECTORIES = new 
PropertyDescriptor.Builder()
+            .name("parameter-group-directories")
+            .displayName("Parameter Group Directories")
+            .description("A comma-separated list of directory absolute paths 
that will map to named parameter groups.  Each directory that contains " +
+                    "files will map to a parameter group, named after the 
innermost directory in the path.  Files inside the directory will map to " +
+                    "parameter names, whose values are the content of each 
respective file.")
+            .addValidator(new MultiDirectoryExistsValidator())
+            .required(true)
+            .build();
+    public static final PropertyDescriptor PARAMETER_VALUE_BYTE_LIMIT = new 
PropertyDescriptor.Builder()
+            .name("parameter-value-byte-limit")
+            .displayName("Parameter Value Byte Limit")
+            .description("The maximum byte size of a parameter value.  Since 
parameter values are pulled from the contents of files, this is a safeguard 
that can " +
+                    "prevent memory issues if large files are included.")
+            .addValidator(StandardValidators.createDataSizeBoundsValidator(1, 
MAX_SIZE_LIMIT))
+            .defaultValue("256 B")
+            .required(true)
+            .build();
+    public static final PropertyDescriptor PARAMETER_VALUE_ENCODING = new 
PropertyDescriptor.Builder()
+            .name("parameter-value-encoding")
+            .displayName("Parameter Value Encoding")
+            .description("Indicates how parameter values are encoded inside 
Parameter files.")
+            .allowableValues(BASE64_ENCODING, PLAIN_TEXT)
+            .defaultValue(BASE64_ENCODING.getValue())
+            .required(true)
+            .build();
+
+    private List<PropertyDescriptor> properties;
+
+    @Override
+    protected void init(final ParameterProviderInitializationContext config) {
+        final List<PropertyDescriptor> properties = new ArrayList<>();
+        properties.add(PARAMETER_GROUP_DIRECTORIES);
+        properties.add(PARAMETER_VALUE_BYTE_LIMIT);
+        properties.add(PARAMETER_VALUE_ENCODING);
+
+        this.properties = Collections.unmodifiableList(properties);
+    }
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return properties;
+    }
+
+    @Override
+    public List<ParameterGroup> fetchParameters(final ConfigurationContext 
context) {
+
+        final List<ParameterGroup> parameterGroups = new ArrayList<>();
+
+        final Collection<File> groupDirectories = getDirectories(context, 
PARAMETER_GROUP_DIRECTORIES);
+        groupDirectories.forEach(directory -> {
+            parameterGroups.add(getParameterGroup(context, directory, 
directory.getName()));
+        });
+        final AtomicInteger groupedParameterCount = new AtomicInteger(0);
+        final Collection<String> groupNames = new HashSet<>();
+        parameterGroups.forEach(group -> {
+            groupedParameterCount.addAndGet(group.getParameters().size());
+            groupNames.add(group.getGroupName());
+        });
+        getLogger().info("Fetched {} parameters.  Group names: {}", 
groupedParameterCount.get(), groupNames);
+        return parameterGroups;
+    }
+
+    @Override
+    public List<ConfigVerificationResult> verify(final ConfigurationContext 
context, final ComponentLog verificationLogger) {
+        final List<ConfigVerificationResult> results = new ArrayList<>();
+
+        try {
+            final List<ParameterGroup> parameterGroups = 
fetchParameters(context);
+            final Set<String> parameterGroupNames = 
parameterGroups.stream().map(ParameterGroup::getGroupName).collect(Collectors.toSet());
+            final long parameterCount = parameterGroups.stream()
+                    .flatMap(group -> group.getParameters().stream())
+                    .count();
+            results.add(new ConfigVerificationResult.Builder()
+                    .outcome(ConfigVerificationResult.Outcome.SUCCESSFUL)
+                    .verificationStepName("Fetch Parameters")
+                    .explanation(String.format("Fetched %s files as 
parameters.", parameterCount))
+                    .build());
+        } catch (final IllegalArgumentException e) {
+            verificationLogger.error("Failed to fetch parameters", e);
+            results.add(new ConfigVerificationResult.Builder()
+                    .outcome(ConfigVerificationResult.Outcome.FAILED)
+                    .verificationStepName("Fetch Parameters")
+                    .explanation("Failed to fetch parameters: " + 
e.getMessage())
+                    .build());
+        }
+        return results;
+    }
+
+    private String getParameterValue(final String rawValue, final 
ParameterValueEncoding encoding) {
+        if (ParameterValueEncoding.BASE64 == encoding) {
+            return new 
String(Base64.getDecoder().decode(rawValue.getBytes(DEFAULT_CHARSET)), 
DEFAULT_CHARSET);
+        }
+        return rawValue;
+    }
+
+    private Collection<File> getDirectories(final ConfigurationContext 
context, final PropertyDescriptor descriptor) {
+        return context.getProperty(descriptor).isSet()
+                ? 
Arrays.stream(context.getProperty(descriptor).getValue().split(",")).map(String::trim).map(File::new).collect(Collectors.toSet())
+                : Collections.emptySet();
+    }
+
+    private ParameterGroup getParameterGroup(final ConfigurationContext 
context, final File directory, final String groupName) {
+        final int parameterSizeLimit = 
context.getProperty(PARAMETER_VALUE_BYTE_LIMIT).asDataSize(DataUnit.B).intValue();
+        final ParameterValueEncoding parameterEncoding = 
ParameterValueEncoding.valueOf(context.getProperty(PARAMETER_VALUE_ENCODING).getValue().toUpperCase());
+
+        final File[] files = directory.listFiles();
+
+        final List<Parameter> parameters = new ArrayList<>();
+        for (final File file : files) {
+            if (file.isDirectory() || file.isHidden()) {
+                continue;
+            }
+
+            final String parameterName = file.getName();
+
+            try (final InputStream in = new BufferedInputStream(new 
LimitingInputStream(new FileInputStream(file), parameterSizeLimit))) {
+                final String rawValue = IOUtils.toString(in, 
Charset.defaultCharset()).trim();
+                final String parameterValue = getParameterValue(rawValue, 
parameterEncoding);
+
+                if (parameterValue.length() >= parameterSizeLimit) {
+                    getLogger().warn("Parameter {} may be truncated at {} 
bytes", parameterName, parameterValue.length());
+                }
+
+                final ParameterDescriptor parameterDescriptor = new 
ParameterDescriptor.Builder().name(parameterName).build();
+                parameters.add(new Parameter(parameterDescriptor, 
parameterValue, null, true));
+            } catch (final IOException e) {
+                getLogger().error("Failed to read file [{}]", new Object[] { 
file }, e);

Review Comment:
   Is it necessary to log an error and throw an exception, or can this log be 
removed?
   ```suggestion
                   getLogger().error("Failed to read file [{}]", file, e);
   ```



##########
nifi-nar-bundles/nifi-standard-bundle/nifi-standard-parameter-providers/src/test/java/org/apache/nifi/parameter/TestEnvironmentVariableParameterProvider.java:
##########
@@ -0,0 +1,96 @@
+package org.apache.nifi.parameter;/*
+ * 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.
+ */
+
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.reporting.InitializationException;
+import org.apache.nifi.util.MockComponentLog;
+import org.apache.nifi.util.MockConfigurationContext;
+import org.apache.nifi.util.MockParameterProviderInitializationContext;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;

Review Comment:
   As a new test, this should be switched to JUnit 5



##########
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/parameter/mock/DummyParameterProvider.java:
##########
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.parameter.mock;
+
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.controller.ControllerService;
+import org.apache.nifi.parameter.AbstractParameterProvider;
+import org.apache.nifi.parameter.Parameter;
+import org.apache.nifi.parameter.ParameterDescriptor;
+import org.apache.nifi.parameter.ParameterGroup;
+import org.apache.nifi.parameter.ParameterProvider;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class DummyParameterProvider extends AbstractParameterProvider 
implements ParameterProvider {

Review Comment:
   Recommend renaming to `PlaceholderParameterProvider` or some other option.



##########
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/VersionedFlowSynchronizer.java:
##########
@@ -195,6 +199,14 @@ public synchronized void sync(final FlowController 
controller, final DataFlow pr
             verifyNoConnectionsWithDataRemoved(existingDataFlow, proposedFlow, 
controller, flowComparison);
 
             synchronizeFlow(controller, existingDataFlow, proposedFlow, 
affectedComponents);
+
+            for (final ParameterProviderNode parameterProviderNode : 
flowManager.getAllParameterProviders()) {
+                try {
+                    parameterProviderNode.fetchParameters();
+                } catch (final Exception e) {
+                    logger.warn("Failed to fetch parameters for provider {}: 
{}", new Object[] { parameterProviderNode.getName(), e.getMessage() }, e);

Review Comment:
   ```suggestion
                       logger.warn("Failed to fetch parameters for provider {}: 
{}", parameterProviderNode.getName(), e.getMessage(), e);
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to