org.apache.nifi.controller.AbstractComponentNode.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.nifi.controller.AbstractComponentNode.java

Source

/*
 * 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.PropertyDescriptor;
import org.apache.nifi.components.ValidationContext;
import org.apache.nifi.components.ValidationResult;
import org.apache.nifi.components.validation.DisabledServiceValidationResult;
import org.apache.nifi.components.validation.ValidationState;
import org.apache.nifi.components.validation.ValidationStatus;
import org.apache.nifi.components.validation.ValidationTrigger;
import org.apache.nifi.controller.service.ControllerServiceDisabledException;
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.ComponentVariableRegistry;
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.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;

public abstract class AbstractComponentNode implements ComponentNode {
    private static final Logger logger = LoggerFactory.getLogger(AbstractComponentNode.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 AtomicReference<ValidationContext> validationContext = new AtomicReference<>();
    private final String componentType;
    private final String componentCanonicalClass;
    private final ComponentVariableRegistry variableRegistry;
    private final ReloadComponent reloadComponent;
    private final ExtensionManager extensionManager;

    private final AtomicBoolean isExtensionMissing;

    private final Lock lock = new ReentrantLock();
    private final ConcurrentMap<PropertyDescriptor, String> properties = new ConcurrentHashMap<>();
    private volatile String additionalResourcesFingerprint;
    private AtomicReference<ValidationState> validationState = new AtomicReference<>(
            new ValidationState(ValidationStatus.VALIDATING, Collections.emptyList()));
    private final ValidationTrigger validationTrigger;
    private volatile boolean triggerValidation = true;

    public AbstractComponentNode(final String id, final ValidationContextFactory validationContextFactory,
            final ControllerServiceProvider serviceProvider, final String componentType,
            final String componentCanonicalClass, final ComponentVariableRegistry variableRegistry,
            final ReloadComponent reloadComponent, final ExtensionManager extensionManager,
            final ValidationTrigger validationTrigger, final boolean isExtensionMissing) {
        this.id = id;
        this.validationContextFactory = validationContextFactory;
        this.serviceProvider = serviceProvider;
        this.name = new AtomicReference<>(componentType);
        this.componentType = componentType;
        this.componentCanonicalClass = componentCanonicalClass;
        this.reloadComponent = reloadComponent;
        this.variableRegistry = variableRegistry;
        this.validationTrigger = validationTrigger;
        this.extensionManager = extensionManager;
        this.isExtensionMissing = new AtomicBoolean(isExtensionMissing);
    }

    @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));
        logger.debug("Resetting Validation State of {} due to setting annotation data", this);
        resetValidationState();
    }

    @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(final Map<String, String> properties,
            final boolean allowRemovalOfRequiredProperties) {
        if (properties == null) {
            return;
        }

        lock.lock();
        try {
            verifyModifiable();

            try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(extensionManager,
                    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(), allowRemovalOfRequiredProperties);
                    } 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) {
                    logger.info("Updating classpath for " + this.componentType + " with the ID "
                            + this.getIdentifier());

                    final Set<URL> additionalUrls = getAdditionalClasspathResources(
                            getComponent().getPropertyDescriptors());
                    try {
                        reload(additionalUrls);
                    } catch (Exception e) {
                        getLogger().error("Error reloading component with id " + id + ": " + e.getMessage(), e);
                    }
                }
            }

            logger.debug("Resetting Validation State of {} due to setting properties", this);
            resetValidationState();
        } 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 {
                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
     * @param allowRemovalOfRequiredProperties whether or not the property should be removed if it's required
     * @return true if removed; false otherwise
     * @throws java.lang.IllegalArgumentException if the name is null
     */
    private boolean removeProperty(final String name, final boolean allowRemovalOfRequiredProperties) {
        if (null == name) {
            throw new IllegalArgumentException("Name can not be null");
        }

        final PropertyDescriptor descriptor = getComponent().getPropertyDescriptor(name);
        String value = null;

        final boolean allowRemoval = allowRemovalOfRequiredProperties || !descriptor.isRequired();
        if (allowRemoval && (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 {
                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(extensionManager,
                getComponent().getClass(), 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()));
    }

    /**
     * Generates fingerprint for the additional urls and compares it with the previous
     * fingerprint value. If the fingerprint values don't match, the function calls the
     * component's reload() to load the newly found resources.
     */
    @Override
    public synchronized void reloadAdditionalResourcesIfNecessary() {
        // Components that don't have any PropertyDescriptors marked `dynamicallyModifiesClasspath`
        // won't have the fingerprint i.e. will be null, in such cases do nothing
        if (additionalResourcesFingerprint == null) {
            return;
        }

        final List<PropertyDescriptor> descriptors = new ArrayList<>(this.getProperties().keySet());
        final Set<URL> additionalUrls = this.getAdditionalClasspathResources(descriptors);

        final String newFingerprint = ClassLoaderUtils.generateAdditionalUrlsFingerprint(additionalUrls);
        if (!StringUtils.equals(additionalResourcesFingerprint, newFingerprint)) {
            setAdditionalResourcesFingerprint(newFingerprint);
            try {
                logger.info(
                        "Updating classpath for " + this.componentType + " with the ID " + this.getIdentifier());
                reload(additionalUrls);
            } catch (Exception e) {
                logger.error("Error reloading component with id " + id + ": " + e.getMessage(), e);
            }
        }
    }

    @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 ComponentNode)) {
            return false;
        }

        final ComponentNode other = (ComponentNode) obj;
        return id.equals(other.getIdentifier());
    }

    @Override
    public String toString() {
        try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(extensionManager,
                getComponent().getClass(), getComponent().getIdentifier())) {
            return getComponent().toString();
        }
    }

    @Override
    public final void performValidation() {
        boolean replaced = false;
        do {
            final ValidationState validationState = getValidationState();

            final ValidationContext validationContext = getValidationContext();
            final Collection<ValidationResult> results = new ArrayList<>();
            try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(extensionManager,
                    getComponent().getClass(), getIdentifier())) {
                final Collection<ValidationResult> validationResults = computeValidationErrors(validationContext);
                results.addAll(validationResults);

                // validate selected controller services implement the API required by the processor
                final Collection<ValidationResult> referencedServiceValidationResults = validateReferencedControllerServices(
                        validationContext);
                results.addAll(referencedServiceValidationResults);
            }

            final ValidationStatus status = results.isEmpty() ? ValidationStatus.VALID : ValidationStatus.INVALID;
            final ValidationState updatedState = new ValidationState(status, results);
            replaced = replaceValidationState(validationState, updatedState);
        } while (!replaced);
    }

    protected Collection<ValidationResult> computeValidationErrors(final ValidationContext validationContext) {
        Throwable failureCause = null;
        try {
            final Collection<ValidationResult> results = getComponent().validate(validationContext);
            logger.debug("Computed validation errors with Validation Context {}; results = {}", validationContext,
                    results);

            return results;
        } catch (final ControllerServiceDisabledException e) {
            getLogger().debug("Failed to perform validation due to " + e, e);
            return Collections.singleton(new DisabledServiceValidationResult("Component",
                    e.getControllerServiceId(),
                    "performing validation depends on referencing a Controller Service that is currently disabled"));
        } catch (final Exception e) {
            // We don't want to log this as an error because we will return a ValidationResult that is
            // invalid. However, we do want to make the stack trace available if needed, so we log it at
            // a debug level.
            getLogger().debug("Failed to perform validation due to " + e, e);
            failureCause = e;
        } catch (final Error e) {
            getLogger().error("Failed to perform validation due to " + e, e);
            failureCause = e;
        }

        return Collections.singleton(new ValidationResult.Builder().subject("Component").valid(false)
                .explanation("Failed to perform validation due to " + failureCause).build());
    }

    protected final Collection<ValidationResult> validateReferencedControllerServices(
            final ValidationContext validationContext) {
        final List<PropertyDescriptor> supportedDescriptors = getComponent().getPropertyDescriptors();
        if (supportedDescriptors == null) {
            return Collections.emptyList();
        }

        final Collection<ValidationResult> validationResults = new ArrayList<>();
        for (final PropertyDescriptor descriptor : supportedDescriptors) {
            if (descriptor.getControllerServiceDefinition() == null) {
                // skip properties that aren't for a controller service
                continue;
            }

            final String controllerServiceId = validationContext.getProperty(descriptor).getValue();
            if (controllerServiceId == null) {
                continue;
            }

            final ControllerServiceNode controllerServiceNode = getControllerServiceProvider()
                    .getControllerServiceNode(controllerServiceId);
            if (controllerServiceNode == null) {
                final ValidationResult result = createInvalidResult(controllerServiceId,
                        descriptor.getDisplayName(), "Invalid Controller Service: " + controllerServiceId
                                + " is not a valid Controller Service Identifier");

                validationResults.add(result);
                continue;
            }

            final ValidationResult apiResult = validateControllerServiceApi(descriptor, controllerServiceNode);
            if (apiResult != null) {
                validationResults.add(apiResult);
                continue;
            }

            if (!controllerServiceNode.isActive()) {
                validationResults
                        .add(new DisabledServiceValidationResult(descriptor.getDisplayName(), controllerServiceId));
            }
        }

        return validationResults;
    }

    private ValidationResult validateControllerServiceApi(final PropertyDescriptor descriptor,
            final ControllerServiceNode controllerServiceNode) {
        final Class<? extends ControllerService> controllerServiceApiClass = descriptor
                .getControllerServiceDefinition();
        final ClassLoader controllerServiceApiClassLoader = controllerServiceApiClass.getClassLoader();
        final ExtensionManager extensionManager = serviceProvider.getExtensionManager();

        final String serviceId = controllerServiceNode.getIdentifier();
        final String propertyName = descriptor.getDisplayName();

        final Bundle controllerServiceApiBundle = extensionManager.getBundle(controllerServiceApiClassLoader);
        if (controllerServiceApiBundle == null) {
            return createInvalidResult(serviceId, propertyName,
                    "Unable to find bundle for ControllerService API class "
                            + controllerServiceApiClass.getCanonicalName());
        }
        final BundleCoordinate controllerServiceApiCoordinate = controllerServiceApiBundle.getBundleDetails()
                .getCoordinate();

        final Bundle controllerServiceBundle = extensionManager
                .getBundle(controllerServiceNode.getBundleCoordinate());
        if (controllerServiceBundle == null) {
            return createInvalidResult(serviceId, propertyName,
                    "Unable to find bundle for coordinate " + controllerServiceNode.getBundleCoordinate());
        }
        final BundleCoordinate controllerServiceCoordinate = controllerServiceBundle.getBundleDetails()
                .getCoordinate();

        final boolean matchesApi = matchesApi(extensionManager, 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();

            return createInvalidResult(serviceId, propertyName, explanation);
        }

        return null;
    }

    private ValidationResult createInvalidResult(final String serviceId, final String propertyName,
            final String explanation) {
        return new ValidationResult.Builder().input(serviceId).subject(propertyName).valid(false)
                .explanation(explanation).build();
    }

    /**
     * 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 ExtensionManager extensionManager, 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(extensionManager,
                getComponent().getClass(), getComponent().getIdentifier())) {
            return getComponent().getPropertyDescriptor(name);
        }
    }

    @Override
    public List<PropertyDescriptor> getPropertyDescriptors() {
        try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(extensionManager,
                getComponent().getClass(), getComponent().getIdentifier())) {
            return getComponent().getPropertyDescriptors();
        }
    }

    private final void onPropertyModified(final PropertyDescriptor descriptor, final String oldValue,
            final String newValue) {
        try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(extensionManager,
                getComponent().getClass(), getComponent().getIdentifier())) {
            getComponent().onPropertyModified(descriptor, oldValue, newValue);
        }
    }

    @Override
    public ValidationStatus getValidationStatus() {
        return validationState.get().getStatus();
    }

    @Override
    public ValidationStatus getValidationStatus(long timeout, TimeUnit timeUnit) {
        long millis = timeUnit.toMillis(timeout);
        final long maxTime = System.currentTimeMillis() + millis;

        synchronized (validationState) {
            while (getValidationStatus() == ValidationStatus.VALIDATING) {
                try {
                    final long waitMillis = Math.max(0, maxTime - System.currentTimeMillis());
                    if (waitMillis <= 0) {
                        break;
                    }

                    validationState.wait(waitMillis);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    return getValidationStatus();
                }
            }

            return getValidationStatus();
        }
    }

    protected ValidationState getValidationState() {
        return validationState.get();
    }

    private boolean replaceValidationState(final ValidationState expectedState, final ValidationState newState) {
        synchronized (validationState) {
            if (validationState.compareAndSet(expectedState, newState)) {
                validationState.notifyAll();
                return true;
            }

            return false;
        }
    }

    protected void resetValidationState() {
        validationContext.set(null);
        validationState.set(new ValidationState(ValidationStatus.VALIDATING, Collections.emptyList()));

        if (isTriggerValidation()) {
            validationTrigger.triggerAsync(this);
        } else {
            logger.debug(
                    "Reset validation state of {} but will not trigger async validation because trigger has been paused",
                    this);
        }
    }

    @Override
    public void pauseValidationTrigger() {
        triggerValidation = false;
    }

    @Override
    public void resumeValidationTrigger() {
        triggerValidation = true;

        final ValidationStatus validationStatus = getValidationStatus();
        if (validationStatus == ValidationStatus.VALIDATING) {
            logger.debug(
                    "Resuming Triggering of Validation State for {}; status is VALIDATING so will trigger async validation now",
                    this);
            validationTrigger.triggerAsync(this);
        } else {
            logger.debug(
                    "Resuming Triggering of Validation State for {}; status is {} so will not trigger async validation now",
                    this, validationStatus);
        }
    }

    private boolean isTriggerValidation() {
        return triggerValidation;
    }

    @Override
    public Collection<ValidationResult> getValidationErrors() {
        return getValidationErrors(Collections.emptySet());
    }

    protected Collection<ValidationResult> getValidationErrors(final Set<ControllerServiceNode> servicesToIgnore) {
        final ValidationState validationState = this.validationState.get();
        if (validationState.getStatus() == ValidationStatus.VALIDATING) {
            return null;
        }

        final Collection<ValidationResult> validationErrors = validationState.getValidationErrors();
        if (servicesToIgnore == null || servicesToIgnore.isEmpty()) {
            return validationErrors;
        }

        final Set<String> ignoredServiceIds = servicesToIgnore.stream().map(ControllerServiceNode::getIdentifier)
                .collect(Collectors.toSet());

        final List<ValidationResult> retainedValidationErrors = new ArrayList<>();
        for (final ValidationResult result : validationErrors) {
            if (!(result instanceof DisabledServiceValidationResult)) {
                retainedValidationErrors.add(result);
                continue;
            }

            final String serviceId = ((DisabledServiceValidationResult) result).getControllerServiceIdentifier();
            if (!ignoredServiceIds.contains(serviceId)) {
                retainedValidationErrors.add(result);
            }
        }

        return retainedValidationErrors;
    }

    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 ValidationContext getValidationContext() {
        while (true) {
            ValidationContext context = this.validationContext.get();
            if (context != null) {
                return context;
            }

            // Use a lock here because we want to prevent calls to getProperties() from happening while setProperties() is also happening.
            final Map<PropertyDescriptor, String> properties;
            lock.lock();
            try {
                properties = getProperties();
            } finally {
                lock.unlock();
            }
            context = getValidationContextFactory().newValidationContext(properties, getAnnotationData(),
                    getProcessGroupIdentifier(), getIdentifier());

            final boolean updated = validationContext.compareAndSet(null, context);
            if (updated) {
                logger.debug("Updating validation context to {}", context);
                return context;
            }
        }
    }

    @Override
    public ComponentVariableRegistry getVariableRegistry() {
        return this.variableRegistry;
    }

    protected ReloadComponent getReloadComponent() {
        return this.reloadComponent;
    }

    protected ExtensionManager getExtensionManager() {
        return this.extensionManager;
    }

    @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()));
            }
        }
    }

    protected void setAdditionalResourcesFingerprint(String additionalResourcesFingerprint) {
        this.additionalResourcesFingerprint = additionalResourcesFingerprint;
    }

}