/* * 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; import org.apache.commons.lang3.StringUtils; import org.apache.nifi.attribute.expression.language.StandardPropertyValue; import org.apache.nifi.bundle.Bundle; import org.apache.nifi.bundle.BundleCoordinate; import org.apache.nifi.components.ConfigurableComponent; import org.apache.nifi.components.PropertyDescriptor; import org.apache.nifi.components.ValidationContext; import org.apache.nifi.components.ValidationResult; import org.apache.nifi.controller.service.ControllerServiceNode; import org.apache.nifi.controller.service.ControllerServiceProvider; import org.apache.nifi.nar.ExtensionManager; import org.apache.nifi.nar.NarCloseable; import org.apache.nifi.registry.VariableRegistry; import org.apache.nifi.util.CharacterFilterUtils; import org.apache.nifi.util.file.classloader.ClassLoaderUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public abstract class AbstractConfiguredComponent implements ConfigurableComponent, ConfiguredComponent { private static final Logger logger = LoggerFactory.getLogger(AbstractConfiguredComponent.class); private final String id; private final ValidationContextFactory validationContextFactory; private final ControllerServiceProvider serviceProvider; private final AtomicReference<String> name; private final AtomicReference<String> annotationData = new AtomicReference<>(); private final String componentType; private final String componentCanonicalClass; private final VariableRegistry variableRegistry; private final ReloadComponent reloadComponent; private final AtomicBoolean isExtensionMissing; private final Lock lock = new ReentrantLock(); private final ConcurrentMap<PropertyDescriptor, String> properties = new ConcurrentHashMap<>(); public AbstractConfiguredComponent(final String id, final ValidationContextFactory validationContextFactory, final ControllerServiceProvider serviceProvider, final String componentType, final String componentCanonicalClass, final VariableRegistry variableRegistry, final ReloadComponent reloadComponent, final boolean isExtensionMissing) { this.id = id; this.validationContextFactory = validationContextFactory; this.serviceProvider = serviceProvider; this.name = new AtomicReference<>(componentType); this.componentType = componentType; this.componentCanonicalClass = componentCanonicalClass; this.variableRegistry = variableRegistry; this.isExtensionMissing = new AtomicBoolean(isExtensionMissing); this.reloadComponent = reloadComponent; } @Override public String getIdentifier() { return id; } @Override public void setExtensionMissing(boolean extensionMissing) { this.isExtensionMissing.set(extensionMissing); } @Override public boolean isExtensionMissing() { return isExtensionMissing.get(); } @Override public String getName() { return name.get(); } @Override public void setName(final String name) { this.name.set(CharacterFilterUtils.filterInvalidXmlCharacters(Objects.requireNonNull(name).intern())); } @Override public String getAnnotationData() { return annotationData.get(); } @Override public void setAnnotationData(final String data) { annotationData.set(CharacterFilterUtils.filterInvalidXmlCharacters(data)); } @Override public Set<URL> getAdditionalClasspathResources(final List<PropertyDescriptor> propertyDescriptors) { final Set<String> modulePaths = new LinkedHashSet<>(); for (final PropertyDescriptor descriptor : propertyDescriptors) { if (descriptor.isDynamicClasspathModifier()) { final String value = getProperty(descriptor); if (!StringUtils.isEmpty(value)) { final StandardPropertyValue propertyValue = new StandardPropertyValue(value, null, variableRegistry); modulePaths.add(propertyValue.evaluateAttributeExpressions().getValue()); } } } final Set<URL> additionalUrls = new LinkedHashSet<>(); try { final URL[] urls = ClassLoaderUtils.getURLsForClasspath(modulePaths, null, true); if (urls != null) { for (final URL url : urls) { additionalUrls.add(url); } } } catch (MalformedURLException mfe) { getLogger().error("Error processing classpath resources for " + id + ": " + mfe.getMessage(), mfe); } return additionalUrls; } @Override public void setProperties(Map<String, String> properties) { if (properties == null) { return; } lock.lock(); try { verifyModifiable(); try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(getComponent().getClass(), id)) { boolean classpathChanged = false; for (final Map.Entry<String, String> entry : properties.entrySet()) { // determine if any of the property changes require resetting the InstanceClassLoader final PropertyDescriptor descriptor = getComponent().getPropertyDescriptor(entry.getKey()); if (descriptor.isDynamicClasspathModifier()) { classpathChanged = true; } if (entry.getKey() != null && entry.getValue() == null) { removeProperty(entry.getKey()); } else if (entry.getKey() != null) { setProperty(entry.getKey(), CharacterFilterUtils.filterInvalidXmlCharacters(entry.getValue())); } } // if at least one property with dynamicallyModifiesClasspath(true) was set, then reload the component with the new urls if (classpathChanged) { final Set<URL> additionalUrls = getAdditionalClasspathResources(getComponent().getPropertyDescriptors()); try { reload(additionalUrls); } catch (Exception e) { getLogger().error("Error reloading component with id " + id + ": " + e.getMessage(), e); } } } } finally { lock.unlock(); } } // Keep setProperty/removeProperty private so that all calls go through setProperties private void setProperty(final String name, final String value) { if (null == name || null == value) { throw new IllegalArgumentException("Name or Value can not be null"); } final PropertyDescriptor descriptor = getComponent().getPropertyDescriptor(name); final String oldValue = properties.put(descriptor, value); if (!value.equals(oldValue)) { if (descriptor.getControllerServiceDefinition() != null) { if (oldValue != null) { final ControllerServiceNode oldNode = serviceProvider.getControllerServiceNode(oldValue); if (oldNode != null) { oldNode.removeReference(this); } } final ControllerServiceNode newNode = serviceProvider.getControllerServiceNode(value); if (newNode != null) { newNode.addReference(this); } } try { getComponent().onPropertyModified(descriptor, oldValue, value); } catch (final Exception e) { // nothing really to do here... } } } /** * Removes the property and value for the given property name if a * descriptor and value exists for the given name. If the property is * optional its value might be reset to default or will be removed entirely * if was a dynamic property. * * @param name the property to remove * @return true if removed; false otherwise * @throws java.lang.IllegalArgumentException if the name is null */ private boolean removeProperty(final String name) { if (null == name) { throw new IllegalArgumentException("Name can not be null"); } final PropertyDescriptor descriptor = getComponent().getPropertyDescriptor(name); String value = null; if (!descriptor.isRequired() && (value = properties.remove(descriptor)) != null) { if (descriptor.getControllerServiceDefinition() != null) { if (value != null) { final ControllerServiceNode oldNode = serviceProvider.getControllerServiceNode(value); if (oldNode != null) { oldNode.removeReference(this); } } } try { getComponent().onPropertyModified(descriptor, value, null); } catch (final Exception e) { getLogger().error(e.getMessage(), e); } return true; } return false; } @Override public Map<PropertyDescriptor, String> getProperties() { try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(getComponent().getClass(), getComponent().getIdentifier())) { final List<PropertyDescriptor> supported = getComponent().getPropertyDescriptors(); if (supported == null || supported.isEmpty()) { return Collections.unmodifiableMap(properties); } else { final Map<PropertyDescriptor, String> props = new LinkedHashMap<>(); for (final PropertyDescriptor descriptor : supported) { props.put(descriptor, null); } props.putAll(properties); return props; } } } @Override public String getProperty(final PropertyDescriptor property) { return properties.get(property); } @Override public void refreshProperties() { // use setProperty instead of setProperties so we can bypass the class loading logic getProperties().entrySet().stream() .filter(e -> e.getKey() != null && e.getValue() != null) .forEach(e -> setProperty(e.getKey().getName(), e.getValue())); } @Override public int hashCode() { return 273171 * id.hashCode(); } @Override public boolean equals(final Object obj) { if (obj == this) { return true; } if (obj == null) { return false; } if (!(obj instanceof ConfiguredComponent)) { return false; } final ConfiguredComponent other = (ConfiguredComponent) obj; return id.equals(other.getIdentifier()); } @Override public String toString() { try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(getComponent().getClass(), getComponent().getIdentifier())) { return getComponent().toString(); } } @Override public Collection<ValidationResult> validate(final ValidationContext context) { try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(getComponent().getClass(), getComponent().getIdentifier())) { final Collection<ValidationResult> validationResults = getComponent().validate(context); // validate selected controller services implement the API required by the processor final List<PropertyDescriptor> supportedDescriptors = getComponent().getPropertyDescriptors(); if (null != supportedDescriptors) { for (final PropertyDescriptor descriptor : supportedDescriptors) { if (descriptor.getControllerServiceDefinition() == null) { // skip properties that aren't for a controller service continue; } final String controllerServiceId = context.getProperty(descriptor).getValue(); if (controllerServiceId == null) { // if the property value is null we should already have a validation error continue; } final ControllerServiceNode controllerServiceNode = getControllerServiceProvider().getControllerServiceNode(controllerServiceId); if (controllerServiceNode == null) { // if the node was null we should already have a validation error continue; } final Class<? extends ControllerService> controllerServiceApiClass = descriptor.getControllerServiceDefinition(); final ClassLoader controllerServiceApiClassLoader = controllerServiceApiClass.getClassLoader(); final Bundle controllerServiceApiBundle = ExtensionManager.getBundle(controllerServiceApiClassLoader); final BundleCoordinate controllerServiceApiCoordinate = controllerServiceApiBundle.getBundleDetails().getCoordinate(); final Bundle controllerServiceBundle = ExtensionManager.getBundle(controllerServiceNode.getBundleCoordinate()); final BundleCoordinate controllerServiceCoordinate = controllerServiceBundle.getBundleDetails().getCoordinate(); final boolean matchesApi = matchesApi(controllerServiceBundle, controllerServiceApiCoordinate); if (!matchesApi) { final String controllerServiceType = controllerServiceNode.getComponentType(); final String controllerServiceApiType = controllerServiceApiClass.getSimpleName(); final String explanation = new StringBuilder() .append(controllerServiceType).append(" - ").append(controllerServiceCoordinate.getVersion()) .append(" from ").append(controllerServiceCoordinate.getGroup()).append(" - ").append(controllerServiceCoordinate.getId()) .append(" is not compatible with ").append(controllerServiceApiType).append(" - ").append(controllerServiceApiCoordinate.getVersion()) .append(" from ").append(controllerServiceApiCoordinate.getGroup()).append(" - ").append(controllerServiceApiCoordinate.getId()) .toString(); validationResults.add(new ValidationResult.Builder() .input(controllerServiceId) .subject(descriptor.getDisplayName()) .valid(false) .explanation(explanation) .build()); } } } return validationResults; } } /** * Determines if the given controller service node has the required API as an ancestor. * * @param controllerServiceImplBundle the bundle of a controller service being referenced by a processor * @param requiredApiCoordinate the controller service API required by the processor * @return true if the controller service node has the require API as an ancestor, false otherwise */ private boolean matchesApi(final Bundle controllerServiceImplBundle, final BundleCoordinate requiredApiCoordinate) { // start with the coordinate of the controller service for cases where the API and service are in the same bundle BundleCoordinate controllerServiceDependencyCoordinate = controllerServiceImplBundle.getBundleDetails().getCoordinate(); boolean foundApiDependency = false; while (controllerServiceDependencyCoordinate != null) { // determine if the dependency coordinate matches the required API if (requiredApiCoordinate.equals(controllerServiceDependencyCoordinate)) { foundApiDependency = true; break; } // move to the next dependency in the chain, or stop if null final Bundle controllerServiceDependencyBundle = ExtensionManager.getBundle(controllerServiceDependencyCoordinate); if (controllerServiceDependencyBundle == null) { controllerServiceDependencyCoordinate = null; } else { controllerServiceDependencyCoordinate = controllerServiceDependencyBundle.getBundleDetails().getDependencyCoordinate(); } } return foundApiDependency; } @Override public PropertyDescriptor getPropertyDescriptor(final String name) { try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(getComponent().getClass(), getComponent().getIdentifier())) { return getComponent().getPropertyDescriptor(name); } } @Override public void onPropertyModified(final PropertyDescriptor descriptor, final String oldValue, final String newValue) { try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(getComponent().getClass(), getComponent().getIdentifier())) { getComponent().onPropertyModified(descriptor, oldValue, newValue); } } @Override public List<PropertyDescriptor> getPropertyDescriptors() { try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(getComponent().getClass(), getComponent().getIdentifier())) { return getComponent().getPropertyDescriptors(); } } @Override public boolean isValid() { final Collection<ValidationResult> validationResults = validate(validationContextFactory.newValidationContext( getProperties(), getAnnotationData(), getProcessGroupIdentifier(), getIdentifier())); for (final ValidationResult result : validationResults) { if (!result.isValid()) { return false; } } return true; } @Override public Collection<ValidationResult> getValidationErrors() { return getValidationErrors(Collections.<String>emptySet()); } public Collection<ValidationResult> getValidationErrors(final Set<String> serviceIdentifiersNotToValidate) { final List<ValidationResult> results = new ArrayList<>(); lock.lock(); try { final ValidationContext validationContext = validationContextFactory.newValidationContext( serviceIdentifiersNotToValidate, getProperties(), getAnnotationData(), getProcessGroupIdentifier(), getIdentifier()); final Collection<ValidationResult> validationResults; try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(getComponent().getClass(), getComponent().getIdentifier())) { validationResults = getComponent().validate(validationContext); } for (final ValidationResult result : validationResults) { if (!result.isValid()) { results.add(result); } } } catch (final Throwable t) { logger.error("Failed to perform validation of " + this, t); results.add(new ValidationResult.Builder().explanation("Failed to run validation due to " + t.toString()).valid(false).build()); } finally { lock.unlock(); } return results; } public abstract void verifyModifiable() throws IllegalStateException; /** * */ ControllerServiceProvider getControllerServiceProvider() { return this.serviceProvider; } @Override public String getCanonicalClassName() { return componentCanonicalClass; } @Override public String getComponentType() { return componentType; } protected ValidationContextFactory getValidationContextFactory() { return this.validationContextFactory; } protected VariableRegistry getVariableRegistry() { return this.variableRegistry; } protected ReloadComponent getReloadComponent() { return this.reloadComponent; } @Override public void verifyCanUpdateBundle(final BundleCoordinate incomingCoordinate) throws IllegalArgumentException { final BundleCoordinate existingCoordinate = getBundleCoordinate(); // determine if this update is changing the bundle for the processor if (!existingCoordinate.equals(incomingCoordinate)) { // if it is changing the bundle, only allow it to change to a different version within same group and id if (!existingCoordinate.getGroup().equals(incomingCoordinate.getGroup()) || !existingCoordinate.getId().equals(incomingCoordinate.getId())) { throw new IllegalArgumentException(String.format( "Unable to update component %s from %s to %s because bundle group and id must be the same.", getIdentifier(), existingCoordinate.getCoordinate(), incomingCoordinate.getCoordinate())); } } } }