blob: 1bd149f7a9cb1347795aba3682ea91a25a78d063 [file] [log] [blame]
/*
* 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.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.components.PropertyDescriptor;
import org.apache.nifi.controller.ComponentNode;
import org.apache.nifi.controller.ProcessorNode;
import org.apache.nifi.controller.PropertyConfiguration;
import org.apache.nifi.controller.service.ControllerServiceNode;
import org.apache.nifi.controller.service.ControllerServiceState;
import org.apache.nifi.groups.ProcessGroup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class StandardParameterContext implements ParameterContext {
private static final Logger logger = LoggerFactory.getLogger(StandardParameterContext.class);
private final String id;
private final ParameterReferenceManager parameterReferenceManager;
private final Authorizable parentAuthorizable;
private String name;
private long version = 0L;
private final Map<ParameterDescriptor, Parameter> parameters = new LinkedHashMap<>();
private volatile String description;
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
public StandardParameterContext(final String id, final String name, final ParameterReferenceManager parameterReferenceManager, final Authorizable parentAuthorizable) {
this.id = Objects.requireNonNull(id);
this.name = Objects.requireNonNull(name);
this.parameterReferenceManager = parameterReferenceManager;
this.parentAuthorizable = parentAuthorizable;
}
@Override
public String getIdentifier() {
return id;
}
@Override
public String getProcessGroupIdentifier() {
return null;
}
public String getName() {
readLock.lock();
try {
return name;
} finally {
readLock.unlock();
}
}
public void setName(final String name) {
writeLock.lock();
try {
this.version++;
this.name = name;
} finally {
writeLock.unlock();
}
}
@Override
public void setDescription(String description) {
this.description = description;
}
@Override
public String getDescription() {
return description;
}
public void setParameters(final Map<String, Parameter> updatedParameters) {
final Map<String, ParameterUpdate> parameterUpdates = new HashMap<>();
boolean changeAffectingComponents = false;
writeLock.lock();
try {
this.version++;
verifyCanSetParameters(updatedParameters);
for (final Map.Entry<String, Parameter> entry : updatedParameters.entrySet()) {
final String parameterName = entry.getKey();
final Parameter parameter = entry.getValue();
if (parameter == null) {
changeAffectingComponents = true;
final ParameterDescriptor parameterDescriptor = new ParameterDescriptor.Builder().name(parameterName).build();
final Parameter oldParameter = parameters.remove(parameterDescriptor);
parameterUpdates.put(parameterName, new StandardParameterUpdate(parameterName, oldParameter.getValue(), null, parameterDescriptor.isSensitive()));
} else {
final Parameter updatedParameter = createFullyPopulatedParameter(parameter);
final Parameter oldParameter = parameters.put(updatedParameter.getDescriptor(), updatedParameter);
if (oldParameter == null || !Objects.equals(oldParameter.getValue(), updatedParameter.getValue())) {
changeAffectingComponents = true;
final String previousValue = oldParameter == null ? null : oldParameter.getValue();
parameterUpdates.put(parameterName, new StandardParameterUpdate(parameterName, previousValue, updatedParameter.getValue(), updatedParameter.getDescriptor().isSensitive()));
}
}
}
} finally {
writeLock.unlock();
}
if (changeAffectingComponents) {
logger.debug("Parameter Context {} was updated. {} parameters changed ({}). Notifying all affected components.", this, parameterUpdates.size(), parameterUpdates);
for (final ProcessGroup processGroup : parameterReferenceManager.getProcessGroupsBound(this)) {
try {
processGroup.onParameterContextUpdated(parameterUpdates);
} catch (final Exception e) {
logger.error("Failed to notify {} that Parameter Context was updated", processGroup, e);
}
}
} else {
logger.debug("Parameter Context {} was updated. {} parameters changed ({}). No existing components are affected.", this, parameterUpdates.size(), parameterUpdates);
}
}
/**
* When updating a Parameter, the provided 'updated' Parameter may or may not contain a value. This is done because once a Parameter is set,
* a user may want to change the description of the Parameter but cannot include the value of the Parameter in the request if the Parameter is sensitive (because
* the value of the Parameter, when retrieved, is masked for sensitive Parameters). As a result, a call may be made to {@link #setParameters(Map)} that includes
* a Parameter whose value is <code>null</code>. If we encounter this, we do not want to change the value of the Parameter, so we want to insert into our Map
* a Parameter object whose value is equal to the current value for that Parameter (if any). This method, then, takes a Parameter whose value may or may not be
* populated and returns a Parameter whose value is populated, if there is an appropriate value to populate it with.
*
* @param proposedParameter the proposed parameter
* @return a Parameter whose descriptor is the same as the given proposed parameter but whose value has been populated based on the existing value for that Parameter (if any)
* if the given proposed parameter does not have its value populated
*/
private Parameter createFullyPopulatedParameter(final Parameter proposedParameter) {
final ParameterDescriptor descriptor = getFullyPopulatedDescriptor(proposedParameter);
final String value = getFullyPopulatedValue(proposedParameter);
return new Parameter(descriptor, value);
}
private String getFullyPopulatedValue(final Parameter proposedParameter) {
return proposedParameter.getValue();
}
private ParameterDescriptor getFullyPopulatedDescriptor(final Parameter proposedParameter) {
final ParameterDescriptor descriptor = proposedParameter.getDescriptor();
if (descriptor.getDescription() != null) {
return descriptor;
}
final Parameter oldParameter = parameters.get(proposedParameter.getDescriptor());
// We know that the Parameters have the same name, since this is what the Descriptor's hashCode & equality are based on. The only thing that may be different
// is the description. And since the proposed Parameter does not have a Description, we want to use whatever is currently set.
return oldParameter == null ? descriptor : oldParameter.getDescriptor();
}
@Override
public long getVersion() {
readLock.lock();
try {
return version;
} finally {
readLock.unlock();
}
}
public Optional<Parameter> getParameter(final String parameterName) {
readLock.lock();
try {
final ParameterDescriptor descriptor = new ParameterDescriptor.Builder().name(parameterName).build();
return getParameter(descriptor);
} finally {
readLock.unlock();
}
}
@Override
public boolean isEmpty() {
readLock.lock();
try {
return parameters.isEmpty();
} finally {
readLock.unlock();
}
}
public Optional<Parameter> getParameter(final ParameterDescriptor parameterDescriptor) {
readLock.lock();
try {
// When Expression Language is used, the Parameter may require being escaped.
// This is the case, for instance, if a Parameter name has a space in it.
// Because of this, we may have a case where we attempt to get a Parameter by name
// and that Parameter name is enclosed within single tick marks, as a way of escaping
// the name via the Expression Language. In this case, we want to strip out those
// escaping tick marks and use just the raw name for looking up the Parameter.
final ParameterDescriptor unescaped = unescape(parameterDescriptor);
return Optional.ofNullable(parameters.get(unescaped));
} finally {
readLock.unlock();
}
}
private ParameterDescriptor unescape(final ParameterDescriptor descriptor) {
final String parameterName = descriptor.getName().trim();
if ((parameterName.startsWith("'") && parameterName.endsWith("'")) || (parameterName.startsWith("\"") && parameterName.endsWith("\""))) {
final String stripped = parameterName.substring(1, parameterName.length() - 1);
return new ParameterDescriptor.Builder()
.from(descriptor)
.name(stripped)
.build();
}
return descriptor;
}
@Override
public Map<ParameterDescriptor, Parameter> getParameters() {
readLock.lock();
try {
return new LinkedHashMap<>(parameters);
} finally {
readLock.unlock();
}
}
@Override
public ParameterReferenceManager getParameterReferenceManager() {
return parameterReferenceManager;
}
@Override
public void verifyCanSetParameters(final Map<String, Parameter> updatedParameters) {
// Ensure that the updated parameters will not result in changing the sensitivity flag of any parameter.
for (final Map.Entry<String, Parameter> entry : updatedParameters.entrySet()) {
final String parameterName = entry.getKey();
final Parameter parameter = entry.getValue();
if (parameter == null) {
// parameter is being deleted.
validateReferencingComponents(parameterName, null,"remove");
continue;
}
if (!Objects.equals(parameterName, parameter.getDescriptor().getName())) {
throw new IllegalArgumentException("Parameter '" + parameterName + "' was specified with the wrong key in the Map");
}
validateSensitiveFlag(parameter);
validateReferencingComponents(parameterName, parameter, "update");
}
}
private void validateSensitiveFlag(final Parameter updatedParameter) {
final ParameterDescriptor updatedDescriptor = updatedParameter.getDescriptor();
final Parameter existingParameter = parameters.get(updatedDescriptor);
if (existingParameter == null) {
return;
}
final ParameterDescriptor existingDescriptor = existingParameter.getDescriptor();
if (existingDescriptor.isSensitive() != updatedDescriptor.isSensitive() && updatedParameter.getValue() != null) {
final String existingSensitiveDescription = existingDescriptor.isSensitive() ? "sensitive" : "not sensitive";
final String updatedSensitiveDescription = updatedDescriptor.isSensitive() ? "sensitive" : "not sensitive";
throw new IllegalStateException("Cannot update Parameters because doing so would change Parameter '" + existingDescriptor.getName() + "' from " + existingSensitiveDescription
+ " to " + updatedSensitiveDescription);
}
}
private void validateReferencingComponents(final String parameterName, final Parameter parameter, final String parameterAction) {
for (final ProcessorNode procNode : parameterReferenceManager.getProcessorsReferencing(this, parameterName)) {
if (procNode.isRunning()) {
throw new IllegalStateException("Cannot " + parameterAction + " parameter '" + parameterName + "' because it is referenced by " + procNode + ", which is currently running");
}
if (parameter != null) {
validateParameterSensitivity(parameter, procNode);
}
}
for (final ControllerServiceNode serviceNode : parameterReferenceManager.getControllerServicesReferencing(this, parameterName)) {
final ControllerServiceState serviceState = serviceNode.getState();
if (serviceState != ControllerServiceState.DISABLED) {
throw new IllegalStateException("Cannot " + parameterAction + " parameter '" + parameterName + "' because it is referenced by "
+ serviceNode + ", which currently has a state of " + serviceState);
}
if (parameter != null) {
validateParameterSensitivity(parameter, serviceNode);
}
}
}
private void validateParameterSensitivity(final Parameter parameter, final ComponentNode componentNode) {
final String paramName = parameter.getDescriptor().getName();
for (final Map.Entry<PropertyDescriptor, PropertyConfiguration> entry : componentNode.getProperties().entrySet()) {
final PropertyConfiguration configuration = entry.getValue();
if (configuration == null) {
continue;
}
for (final ParameterReference reference : configuration.getParameterReferences()) {
if (parameter.getDescriptor().getName().equals(reference.getParameterName())) {
final PropertyDescriptor propertyDescriptor = entry.getKey();
if (propertyDescriptor.isSensitive() && !parameter.getDescriptor().isSensitive()) {
throw new IllegalStateException("Cannot add Parameter with name '" + paramName + "' unless that Parameter is Sensitive because a Parameter with that name is already " +
"referenced from a Sensitive Property");
}
if (!propertyDescriptor.isSensitive() && parameter.getDescriptor().isSensitive()) {
throw new IllegalStateException("Cannot add Parameter with name '" + paramName + "' unless that Parameter is Not Sensitive because a Parameter with that name is already " +
"referenced from a Property that is not Sensitive");
}
}
}
}
}
@Override
public String toString() {
return "StandardParameterContext[name=" + name + "]";
}
@Override
public Authorizable getParentAuthorizable() {
return new Authorizable() {
@Override
public Authorizable getParentAuthorizable() {
return parentAuthorizable;
}
@Override
public Resource getResource() {
return ResourceFactory.getParameterContextsResource();
}
};
}
@Override
public Resource getResource() {
return ResourceFactory.getComponentResource(ResourceType.ParameterContext, getIdentifier(), getName());
}
}