blob: 21e40111b473522eef91e22b45206ebbe8f36e90 [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.fingerprint;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.bundle.BundleCoordinate;
import org.apache.nifi.components.ConfigurableComponent;
import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.controller.FlowController;
import org.apache.nifi.controller.serialization.FlowEncodingVersion;
import org.apache.nifi.controller.serialization.FlowFromDOMFactory;
import org.apache.nifi.encrypt.PropertyEncryptor;
import org.apache.nifi.encrypt.SensitiveValueEncoder;
import org.apache.nifi.nar.ExtensionManager;
import org.apache.nifi.security.xml.XmlUtils;
import org.apache.nifi.util.BundleUtils;
import org.apache.nifi.util.DomUtils;
import org.apache.nifi.util.LoggingXmlParserErrorHandler;
import org.apache.nifi.web.api.dto.BundleDTO;
import org.apache.nifi.web.api.dto.ControllerServiceDTO;
import org.apache.nifi.web.api.dto.ReportingTaskDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.stream.Stream;
/**
* <p>Creates a fingerprint of a flow.xml. The order of elements or attributes in the flow.xml does not influence the fingerprint generation.
*
* <p>Only items in the flow.xml that influence the processing of data are incorporated into the fingerprint.
* Examples of items involved in the fingerprint are: processor IDs, processor relationships, and processor properties.
* Examples of items not involved in the fingerprint are: items in the processor "comments" tabs, position information, flow controller settings, and counters.
*
* <p>The determination for making items into the fingerprint is whether we can
* easily change the setting in order to inherit the cluster's flow.
* For example, if the component has to be stopped to apply the change and started again,
* then the item should be included in a fingerprint.
*/
public class FingerprintFactory {
/*
* Developer Note: This class should be changed with care and coordinated
* with all classes that use fingerprinting. Improper coordination may
* lead to orphaning flow files, especially when flows are reloaded in a
* clustered environment.
*/
// no fingerprint value
public static final String NO_VALUE = "NO_VALUE";
static final String FLOW_CONFIG_XSD = "/FlowConfiguration.xsd";
private static final String ENCRYPTED_VALUE_PREFIX = "enc{";
private static final String ENCRYPTED_VALUE_SUFFIX = "}";
private final PropertyEncryptor encryptor;
private final DocumentBuilder flowConfigDocBuilder;
private final ExtensionManager extensionManager;
private final SensitiveValueEncoder sensitiveValueEncoder;
private static final Logger logger = LoggerFactory.getLogger(FingerprintFactory.class);
public FingerprintFactory(final PropertyEncryptor encryptor, final ExtensionManager extensionManager, final SensitiveValueEncoder sensitiveValueEncoder) {
this.encryptor = encryptor;
this.extensionManager = extensionManager;
this.sensitiveValueEncoder = sensitiveValueEncoder;
final SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
final Schema schema;
try {
schema = schemaFactory.newSchema(FingerprintFactory.class.getResource(FLOW_CONFIG_XSD));
} catch (final Exception e) {
throw new RuntimeException("Failed to parse schema for file flow configuration.", e);
}
try {
flowConfigDocBuilder = XmlUtils.createSafeDocumentBuilder(schema, true);
flowConfigDocBuilder.setErrorHandler(new LoggingXmlParserErrorHandler("Flow Configuration", logger));
} catch (final Exception e) {
throw new RuntimeException("Failed to create document builder for flow configuration.", e);
}
}
public FingerprintFactory(final PropertyEncryptor encryptor, final DocumentBuilder docBuilder, final ExtensionManager extensionManager, final SensitiveValueEncoder sensitiveValueEncoder) {
this.encryptor = encryptor;
this.flowConfigDocBuilder = docBuilder;
this.extensionManager = extensionManager;
this.sensitiveValueEncoder = sensitiveValueEncoder;
}
/**
* Creates a fingerprint of a flow. The order of elements or attributes in the flow does not influence the fingerprint generation.
* This method does not accept a FlowController, which means that Processors cannot be created in order to verify default property
* values, etc. As a result, if Flow A and Flow B are fingerprinted and Flow B, for instance, contains a property with a default value
* that is not present in Flow A, then the two will have different fingerprints.
*
* @param flowBytes the flow represented as bytes
* @return a generated fingerprint
* @throws FingerprintException if the fingerprint failed to be generated
*/
public synchronized String createFingerprint(final byte[] flowBytes) throws FingerprintException {
return createFingerprint(flowBytes, null);
}
/**
* Creates a fingerprint of a flow. The order of elements or attributes in the flow does not influence the fingerprint generation.
*
* @param flowBytes the flow represented as bytes
* @param controller the controller
* @return a generated fingerprint
* @throws FingerprintException if the fingerprint failed to be generated
*/
public synchronized String createFingerprint(final byte[] flowBytes, final FlowController controller) throws FingerprintException {
return createFingerprint(parseFlow(flowBytes), controller);
}
/**
* Creates a fingerprint from an XML document representing the flow.xml.
*
* @param flowDoc the DOM
* @return the fingerprint
*/
public synchronized String createFingerprint(final Document flowDoc, final FlowController controller) {
if (flowDoc == null) {
return "";
}
// builder to hold fingerprint state
final StringBuilder fingerprintBuilder = new StringBuilder();
// add flow controller fingerprint
final Element flowControllerElem = flowDoc.getDocumentElement();
if (flowControllerElem == null) {
logger.warn("Unable to create fingerprint because no 'flowController' element found in XML.");
return "";
}
final FlowEncodingVersion encodingVersion = FlowEncodingVersion.parse(flowControllerElem);
addFlowControllerFingerprint(fingerprintBuilder, flowControllerElem, controller, encodingVersion);
return fingerprintBuilder.toString();
}
/**
* Parse the given flow.xml bytes into a Document instance.
*
* @param flow a flow
* @return the DOM
* @throws FingerprintException if the flow could not be parsed
*/
private Document parseFlow(final byte[] flow) throws FingerprintException {
if (flow == null || flow.length == 0) {
return null;
}
try {
return flowConfigDocBuilder.parse(new ByteArrayInputStream(flow));
} catch (final SAXException | IOException ex) {
throw new FingerprintException(ex);
}
}
private StringBuilder addFlowControllerFingerprint(final StringBuilder builder, final Element flowControllerElem, final FlowController controller, final FlowEncodingVersion encodingVersion) {
// registries
final Element registriesElement = DomUtils.getChild(flowControllerElem, "registries");
if (registriesElement == null) {
builder.append("NO_VALUE");
} else {
final List<Element> flowRegistryElems = DomUtils.getChildElementsByTagName(registriesElement, "flowRegistry");
if (flowRegistryElems.isEmpty()) {
builder.append("NO_VALUE");
} else {
for (final Element flowRegistryElement : flowRegistryElems) {
addFlowRegistryFingerprint(builder, flowRegistryElement);
}
}
}
final Element contextsElement = DomUtils.getChild(flowControllerElem, "parameterContexts");
if (contextsElement == null) {
builder.append("NO_PARAMETER_CONTEXTS");
} else {
final List<Element> parameterContextElements = DomUtils.getChildElementsByTagName(contextsElement, "parameterContext");
if (parameterContextElements.isEmpty()) {
builder.append("NO_PARAMETER_CONTEXTS");
} else {
orderByChildElement(parameterContextElements, "id");
for (final Element parameterContextElement : parameterContextElements) {
addParameterContext(builder, parameterContextElement);
}
}
}
// root group
final Element rootGroupElem = (Element) DomUtils.getChildNodesByTagName(flowControllerElem, "rootGroup").item(0);
addProcessGroupFingerprint(builder, rootGroupElem, encodingVersion);
final Element controllerServicesElem = DomUtils.getChild(flowControllerElem, "controllerServices");
if (controllerServicesElem != null) {
final List<ControllerServiceDTO> serviceDtos = new ArrayList<>();
for (final Element serviceElem : DomUtils.getChildElementsByTagName(controllerServicesElem, "controllerService")) {
final ControllerServiceDTO dto = FlowFromDOMFactory.getControllerService(serviceElem, encryptor, encodingVersion);
serviceDtos.add(dto);
}
Collections.sort(serviceDtos, new Comparator<ControllerServiceDTO>() {
@Override
public int compare(final ControllerServiceDTO o1, final ControllerServiceDTO o2) {
if (o1 == null && o2 == null) {
return 0;
}
if (o1 == null && o2 != null) {
return 1;
}
if (o1 != null && o2 == null) {
return -1;
}
return o1.getId().compareTo(o2.getId());
}
});
for (final ControllerServiceDTO dto : serviceDtos) {
addControllerServiceFingerprint(builder, dto);
}
}
final Element reportingTasksElem = DomUtils.getChild(flowControllerElem, "reportingTasks");
if (reportingTasksElem != null) {
final List<ReportingTaskDTO> reportingTaskDtos = new ArrayList<>();
for (final Element taskElem : DomUtils.getChildElementsByTagName(reportingTasksElem, "reportingTask")) {
final ReportingTaskDTO dto = FlowFromDOMFactory.getReportingTask(taskElem, encryptor, encodingVersion);
reportingTaskDtos.add(dto);
}
Collections.sort(reportingTaskDtos, new Comparator<ReportingTaskDTO>() {
@Override
public int compare(final ReportingTaskDTO o1, final ReportingTaskDTO o2) {
if (o1 == null && o2 == null) {
return 0;
}
if (o1 == null && o2 != null) {
return 1;
}
if (o1 != null && o2 == null) {
return -1;
}
return o1.getId().compareTo(o2.getId());
}
});
for (final ReportingTaskDTO dto : reportingTaskDtos) {
addReportingTaskFingerprint(builder, dto);
}
}
return builder;
}
private void orderByChildElement(final List<Element> toSort, final String childTagName) {
toSort.sort((a, b) -> {
final String valueA = DomUtils.getChildText(a, childTagName);
final String valueB = DomUtils.getChildText(b, childTagName);
return valueA.compareTo(valueB);
});
}
private StringBuilder addParameterContext(final StringBuilder builder, final Element parameterContextElement) {
appendFirstValue(builder, DomUtils.getChildNodesByTagName(parameterContextElement, "id"));
appendFirstValue(builder, DomUtils.getChildNodesByTagName(parameterContextElement, "name"));
appendFirstValue(builder, DomUtils.getChildNodesByTagName(parameterContextElement, "description"));
final List<Element> parameterElements = DomUtils.getChildElementsByTagName(parameterContextElement, "parameter");
if (parameterElements == null || parameterElements.isEmpty()) {
builder.append("NO_PARAMETERS");
} else {
orderByChildElement(parameterElements, "name");
for (final Element parameterElement : parameterElements) {
addParameter(builder, parameterElement);
}
}
return builder;
}
private void addParameter(final StringBuilder builder, final Element parameterElement) {
Stream.of("name", "description", "sensitive").forEach(elementName -> appendFirstValue(builder, DomUtils.getChildNodesByTagName(parameterElement, elementName)));
final String value = DomUtils.getChildText(parameterElement, "value");
if (value == null) {
builder.append("NO_VALUE");
return;
}
// append value
if (isEncrypted(value)) {
// Get a secure, deterministic, loggable representation of this value
builder.append(getLoggableRepresentationOfSensitiveValue(value));
} else {
builder.append(getValue(value, NO_VALUE));
}
}
private StringBuilder addFlowRegistryFingerprint(final StringBuilder builder, final Element flowRegistryElement) {
Stream.of("id", "name", "url", "description").forEach(elementName -> appendFirstValue(builder, DomUtils.getChildNodesByTagName(flowRegistryElement, elementName)));
return builder;
}
StringBuilder addProcessGroupFingerprint(final StringBuilder builder, final Element processGroupElem, final FlowEncodingVersion encodingVersion) throws FingerprintException {
// id
appendFirstValue(builder, DomUtils.getChildNodesByTagName(processGroupElem, "id"));
appendFirstValue(builder, DomUtils.getChildNodesByTagName(processGroupElem, "versionedComponentId"));
appendFirstValue(builder, DomUtils.getChildNodesByTagName(processGroupElem, "parameterContextId"));
appendFirstValue(builder, DomUtils.getChildNodesByTagName(processGroupElem, "flowfileConcurrency"));
appendFirstValue(builder, DomUtils.getChildNodesByTagName(processGroupElem, "flowfileOutboundPolicy"));
appendFirstValue(builder, DomUtils.getChildNodesByTagName(processGroupElem, "defaultFlowFileExpiration"));
appendFirstValue(builder, DomUtils.getChildNodesByTagName(processGroupElem, "defaultBackPressureObjectThreshold"));
appendFirstValue(builder, DomUtils.getChildNodesByTagName(processGroupElem, "defaultBackPressureDataSizeThreshold"));
final Element versionControlInfo = DomUtils.getChild(processGroupElem, "versionControlInformation");
if (versionControlInfo == null) {
builder.append("NO_VERSION_CONTROL_INFORMATION");
} else {
appendFirstValue(builder, DomUtils.getChildNodesByTagName(versionControlInfo, "registryId"));
appendFirstValue(builder, DomUtils.getChildNodesByTagName(versionControlInfo, "bucketId"));
appendFirstValue(builder, DomUtils.getChildNodesByTagName(versionControlInfo, "flowId"));
appendFirstValue(builder, DomUtils.getChildNodesByTagName(versionControlInfo, "version"));
}
// processors
final List<Element> processorElems = DomUtils.getChildElementsByTagName(processGroupElem, "processor");
processorElems.sort(getIdsComparator());
for (final Element processorElem : processorElems) {
addFlowFileProcessorFingerprint(builder, processorElem);
}
// input ports
final NodeList inputPortElems = DomUtils.getChildNodesByTagName(processGroupElem, "inputPort");
final List<Element> sortedInputPortElems = sortElements(inputPortElems, getIdsComparator());
for (final Element inputPortElem : sortedInputPortElems) {
addPortFingerprint(builder, inputPortElem);
}
// labels
final NodeList labelElems = DomUtils.getChildNodesByTagName(processGroupElem, "label");
final List<Element> sortedLabels = sortElements(labelElems, getIdsComparator());
for (final Element labelElem : sortedLabels) {
addLabelFingerprint(builder, labelElem);
}
// output ports
final NodeList outputPortElems = DomUtils.getChildNodesByTagName(processGroupElem, "outputPort");
final List<Element> sortedOutputPortElems = sortElements(outputPortElems, getIdsComparator());
for (final Element outputPortElem : sortedOutputPortElems) {
addPortFingerprint(builder, outputPortElem);
}
// process groups
final NodeList nestedProcessGroupElems = DomUtils.getChildNodesByTagName(processGroupElem, "processGroup");
final List<Element> sortedNestedProcessGroupElems = sortElements(nestedProcessGroupElems, getIdsComparator());
for (final Element nestedProcessGroupElem : sortedNestedProcessGroupElems) {
addProcessGroupFingerprint(builder, nestedProcessGroupElem, encodingVersion);
}
// remote process groups
final NodeList remoteProcessGroupElems = DomUtils.getChildNodesByTagName(processGroupElem, "remoteProcessGroup");
final List<Element> sortedRemoteProcessGroupElems = sortElements(remoteProcessGroupElems, getIdsComparator());
for (final Element remoteProcessGroupElem : sortedRemoteProcessGroupElems) {
addRemoteProcessGroupFingerprint(builder, remoteProcessGroupElem);
}
// connections
final NodeList connectionElems = DomUtils.getChildNodesByTagName(processGroupElem, "connection");
final List<Element> sortedConnectionElems = sortElements(connectionElems, getIdsComparator());
for (final Element connectionElem : sortedConnectionElems) {
addConnectionFingerprint(builder, connectionElem);
}
// funnel
final NodeList funnelElems = DomUtils.getChildNodesByTagName(processGroupElem, "funnel");
final List<Element> sortedFunnelElems = sortElements(funnelElems, getIdsComparator());
for (final Element funnelElem : sortedFunnelElems) {
addFunnelFingerprint(builder, funnelElem);
}
final NodeList controllerServiceElems = DomUtils.getChildNodesByTagName(processGroupElem, "controllerService");
final List<Element> sortedControllerServiceElems = sortElements(controllerServiceElems, getIdsComparator());
for (final Element controllerServiceElem : sortedControllerServiceElems) {
final ControllerServiceDTO dto = FlowFromDOMFactory.getControllerService(controllerServiceElem, encryptor, encodingVersion);
addControllerServiceFingerprint(builder, dto);
}
// add variables
final NodeList variableElems = DomUtils.getChildNodesByTagName(processGroupElem, "variable");
final List<Element> sortedVarList = sortElements(variableElems, getVariableNameComparator());
for (final Element varElem : sortedVarList) {
addVariableFingerprint(builder, varElem);
}
return builder;
}
private void addVariableFingerprint(final StringBuilder builder, final Element variableElement) {
final String variableName = variableElement.getAttribute("name");
final String variableValue = variableElement.getAttribute("value");
builder.append(variableName).append("=").append(variableValue);
}
private StringBuilder addFlowFileProcessorFingerprint(final StringBuilder builder, final Element processorElem) throws FingerprintException {
// id
appendFirstValue(builder, DomUtils.getChildNodesByTagName(processorElem, "id"));
appendFirstValue(builder, DomUtils.getChildNodesByTagName(processorElem, "versionedComponentId"));
// class
final NodeList childNodes = DomUtils.getChildNodesByTagName(processorElem, "class");
final String className = childNodes.item(0).getTextContent();
appendFirstValue(builder, childNodes);
// annotation data
appendFirstValue(builder, DomUtils.getChildNodesByTagName(processorElem, "annotationData"));
// get the bundle details if possible
final BundleDTO bundle = FlowFromDOMFactory.getBundle(DomUtils.getChild(processorElem, "bundle"));
addBundleFingerprint(builder, bundle);
// max concurrent tasks
appendFirstValue(builder, DomUtils.getChildNodesByTagName(processorElem, "maxConcurrentTasks"));
// scheduling period
appendFirstValue(builder, DomUtils.getChildNodesByTagName(processorElem, "schedulingPeriod"));
// penalization period
appendFirstValue(builder, DomUtils.getChildNodesByTagName(processorElem, "penalizationPeriod"));
// yield period
appendFirstValue(builder, DomUtils.getChildNodesByTagName(processorElem, "yieldPeriod"));
// bulletin level
appendFirstValue(builder, DomUtils.getChildNodesByTagName(processorElem, "bulletinLevel"));
// loss tolerant
appendFirstValue(builder, DomUtils.getChildNodesByTagName(processorElem, "lossTolerant"));
// scheduling strategy
appendFirstValue(builder, DomUtils.getChildNodesByTagName(processorElem, "schedulingStrategy"));
// execution node
appendFirstValue(builder, DomUtils.getChildNodesByTagName(processorElem, "executionNode"));
// run duration nanos
appendFirstValue(builder, DomUtils.getChildNodesByTagName(processorElem, "runDurationNanos"));
// get the temp instance of the Processor so that we know the default property values
final BundleCoordinate coordinate = getCoordinate(className, bundle);
final ConfigurableComponent configurableComponent = extensionManager.getTempComponent(className, coordinate);
if (configurableComponent == null) {
logger.warn("Unable to get Processor of type {}; its default properties will be fingerprinted instead of being ignored.", className);
}
// properties
final NodeList propertyElems = DomUtils.getChildNodesByTagName(processorElem, "property");
final List<Element> sortedPropertyElems = sortElements(propertyElems, getProcessorPropertiesComparator());
for (final Element propertyElem : sortedPropertyElems) {
final String propName = DomUtils.getChildElementsByTagName(propertyElem, "name").get(0).getTextContent();
String propValue = getFirstValue(DomUtils.getChildNodesByTagName(propertyElem, "value"), null);
addPropertyFingerprint(builder, configurableComponent, propName, propValue);
}
final NodeList autoTerminateElems = DomUtils.getChildNodesByTagName(processorElem, "autoTerminatedRelationship");
final List<Element> sortedAutoTerminateElems = sortElements(autoTerminateElems, getElementTextComparator());
for (final Element autoTerminateElem : sortedAutoTerminateElems) {
builder.append(autoTerminateElem.getTextContent());
}
return builder;
}
private StringBuilder addPropertyFingerprint(final StringBuilder builder, final ConfigurableComponent component, final String propName, final String propValue) throws FingerprintException {
// If we have a component to use, first determine if the value given is the default value for the specified property.
// If so, we do not add the property to the fingerprint.
// We do this because if a component is updated to add a new property, whenever we connect to the cluster, we have issues because
// the Cluster Coordinator's flow comes from disk, where the flow.xml doesn't have the new property but our FlowController does have the new property.
// This causes the fingerprints not to match. As a result, we just ignore default values, and this resolves the issue.
if (component != null) {
final PropertyDescriptor descriptor = component.getPropertyDescriptor(propName);
if (descriptor != null && propValue != null && propValue.equals(descriptor.getDefaultValue())) {
return builder;
}
}
// check if there is a value
if (propValue == null) {
return builder;
}
// append name
builder.append(propName).append("=");
// append value
if (isEncrypted(propValue)) {
// Get a secure, deterministic, loggable representation of this value
builder.append(getLoggableRepresentationOfSensitiveValue(propValue));
} else {
builder.append(getValue(propValue, NO_VALUE));
}
return builder;
}
/**
* Returns a securely-derived, deterministic value from the provided encrypted property
* value. This is because the flow fingerprint is displayed in the log if NiFi has
* trouble inheriting a flow, so the sensitive value should not be disclosed through the
* log. However, the equality or difference of the sensitive value can influence in the
* inheritability of the flow, so it cannot be ignored completely.
* <p>
* The specific derivation process is unimportant as long as it is a salted,
* cryptographically-secure hash function with an iteration cost sufficient for password
* storage in other applications.
*
* @param encryptedPropertyValue the encrypted property value
* @return a deterministic string value which represents this input but is safe to print in a log
*/
private String getLoggableRepresentationOfSensitiveValue(String encryptedPropertyValue) {
final String plaintextValue = decrypt(encryptedPropertyValue);
return sensitiveValueEncoder.getEncoded(plaintextValue);
}
private StringBuilder addPortFingerprint(final StringBuilder builder, final Element portElem) throws FingerprintException {
// id
appendFirstValue(builder, DomUtils.getChildNodesByTagName(portElem, "id"));
appendFirstValue(builder, DomUtils.getChildNodesByTagName(portElem, "versionedComponentId"));
appendFirstValue(builder, DomUtils.getChildNodesByTagName(portElem, "name"));
appendFirstValue(builder, DomUtils.getChildNodesByTagName(portElem, "allowRemoteAccess"));
final NodeList userAccessControlNodeList = DomUtils.getChildNodesByTagName(portElem, "userAccessControl");
if (userAccessControlNodeList == null || userAccessControlNodeList.getLength() == 0) {
builder.append("NO_USER_ACCESS_CONTROL");
} else {
final List<String> sortedAccessControl = new ArrayList<>();
for (int i = 0; i < userAccessControlNodeList.getLength(); i++) {
sortedAccessControl.add(userAccessControlNodeList.item(i).getTextContent());
}
Collections.sort(sortedAccessControl);
for (final String user : sortedAccessControl) {
builder.append(user);
}
}
final NodeList groupAccessControlNodeList = DomUtils.getChildNodesByTagName(portElem, "userAccessControl");
if (groupAccessControlNodeList == null || groupAccessControlNodeList.getLength() == 0) {
builder.append("NO_GROUP_ACCESS_CONTROL");
} else {
final List<String> sortedAccessControl = new ArrayList<>();
for (int i = 0; i < groupAccessControlNodeList.getLength(); i++) {
sortedAccessControl.add(groupAccessControlNodeList.item(i).getTextContent());
}
Collections.sort(sortedAccessControl);
for (final String user : sortedAccessControl) {
builder.append(user);
}
}
return builder;
}
private StringBuilder addLabelFingerprint(final StringBuilder builder, final Element labelElem) {
appendFirstValue(builder, DomUtils.getChildNodesByTagName(labelElem, "id"));
appendFirstValue(builder, DomUtils.getChildNodesByTagName(labelElem, "versionedComponentId"));
appendFirstValue(builder, DomUtils.getChildNodesByTagName(labelElem, "value"));
return builder;
}
private StringBuilder addRemoteProcessGroupFingerprint(final StringBuilder builder, final Element remoteProcessGroupElem) throws FingerprintException {
for (String tagName : new String[]{"id", "versionedComponentId", "urls", "networkInterface", "timeout", "yieldPeriod",
"transportProtocol", "proxyHost", "proxyPort", "proxyUser", "proxyPassword"}) {
final String value = getFirstValue(DomUtils.getChildNodesByTagName(remoteProcessGroupElem, tagName));
if (isEncrypted(value)) {
builder.append(getLoggableRepresentationOfSensitiveValue(value));
} else {
builder.append(value);
}
}
final NodeList inputPortList = DomUtils.getChildNodesByTagName(remoteProcessGroupElem, "inputPort");
final NodeList outputPortList = DomUtils.getChildNodesByTagName(remoteProcessGroupElem, "outputPort");
final Comparator<Element> portComparator = new Comparator<Element>() {
@Override
public int compare(final Element o1, final Element o2) {
if (o1 == null && o2 == null) {
return 0;
}
if (o1 == null) {
return 1;
}
if (o2 == null) {
return -1;
}
NodeList nameList1 = DomUtils.getChildNodesByTagName(o1, "name");
NodeList nameList2 = DomUtils.getChildNodesByTagName(o2, "name");
if (nameList1.getLength() == 0 && nameList2.getLength() == 0) {
return 0;
}
if (nameList1.getLength() == 0) {
return 1;
}
if (nameList2.getLength() == 0) {
return -1;
}
return nameList1.item(0).getTextContent().compareTo(nameList2.item(0).getTextContent());
}
};
final List<Element> sortedInputPorts = new ArrayList<>(inputPortList.getLength());
for (int i = 0; i < inputPortList.getLength(); i++) {
sortedInputPorts.add((Element) inputPortList.item(i));
}
Collections.sort(sortedInputPorts, portComparator);
final List<Element> sortedOutputPorts = new ArrayList<>(outputPortList.getLength());
for (int i = 0; i < outputPortList.getLength(); i++) {
sortedOutputPorts.add((Element) outputPortList.item(i));
}
Collections.sort(sortedOutputPorts, portComparator);
for (final Element inputPortElement : sortedInputPorts) {
addRemoteGroupPortFingerprint(builder, inputPortElement);
}
for (final Element outputPortElement : sortedOutputPorts) {
addRemoteGroupPortFingerprint(builder, outputPortElement);
}
return builder;
}
private StringBuilder addRemoteGroupPortFingerprint(final StringBuilder builder, final Element remoteGroupPortElement) {
for (final String childName : new String[]{"id", "targetId", "versionedComponentId", "maxConcurrentTasks", "useCompression", "batchCount", "batchSize", "batchDuration"}) {
appendFirstValue(builder, DomUtils.getChildNodesByTagName(remoteGroupPortElement, childName));
}
return builder;
}
private StringBuilder addConnectionFingerprint(final StringBuilder builder, final Element connectionElem) throws FingerprintException {
// id
appendFirstValue(builder, DomUtils.getChildNodesByTagName(connectionElem, "id"));
appendFirstValue(builder, DomUtils.getChildNodesByTagName(connectionElem, "versionedComponentId"));
// source id
appendFirstValue(builder, DomUtils.getChildNodesByTagName(connectionElem, "sourceId"));
// source group id
appendFirstValue(builder, DomUtils.getChildNodesByTagName(connectionElem, "sourceGroupId"));
// source type
appendFirstValue(builder, DomUtils.getChildNodesByTagName(connectionElem, "sourceType"));
// destination id
appendFirstValue(builder, DomUtils.getChildNodesByTagName(connectionElem, "destinationId"));
// destination group id
appendFirstValue(builder, DomUtils.getChildNodesByTagName(connectionElem, "destinationGroupId"));
// destination type
appendFirstValue(builder, DomUtils.getChildNodesByTagName(connectionElem, "destinationType"));
appendFirstValue(builder, DomUtils.getChildNodesByTagName(connectionElem, "name"));
appendFirstValue(builder, DomUtils.getChildNodesByTagName(connectionElem, "loadBalanceStrategy"));
appendFirstValue(builder, DomUtils.getChildNodesByTagName(connectionElem, "partitioningAttribute"));
appendFirstValue(builder, DomUtils.getChildNodesByTagName(connectionElem, "loadBalanceCompression"));
// relationships
final NodeList relationshipElems = DomUtils.getChildNodesByTagName(connectionElem, "relationship");
final List<Element> sortedRelationshipElems = sortElements(relationshipElems, getElementTextComparator());
for (final Element relationshipElem : sortedRelationshipElems) {
builder.append(getValue(relationshipElem, NO_VALUE));
}
return builder;
}
private StringBuilder addFunnelFingerprint(final StringBuilder builder, final Element funnelElem) throws FingerprintException {
// id
appendFirstValue(builder, DomUtils.getChildNodesByTagName(funnelElem, "id"));
appendFirstValue(builder, DomUtils.getChildNodesByTagName(funnelElem, "versionedComponentId"));
return builder;
}
private void addControllerServiceFingerprint(final StringBuilder builder, final ControllerServiceDTO dto) {
builder.append(dto.getId());
builder.append(dto.getVersionedComponentId());
builder.append(dto.getType());
builder.append(dto.getName());
addBundleFingerprint(builder, dto.getBundle());
builder.append(dto.getComments());
builder.append(dto.getAnnotationData());
builder.append(dto.getState());
// get the temp instance of the ControllerService so that we know the default property values
final BundleCoordinate coordinate = getCoordinate(dto.getType(), dto.getBundle());
final ConfigurableComponent configurableComponent = extensionManager.getTempComponent(dto.getType(), coordinate);
if (configurableComponent == null) {
logger.warn("Unable to get ControllerService of type {}; its default properties will be fingerprinted instead of being ignored.", dto.getType());
}
addPropertiesFingerprint(builder, configurableComponent, dto.getProperties());
}
private void addPropertiesFingerprint(final StringBuilder builder, final ConfigurableComponent component, final Map<String, String> properties) {
if (properties == null) {
builder.append("NO_PROPERTIES");
} else {
final SortedMap<String, String> sortedProps = new TreeMap<>(properties);
for (final Map.Entry<String, String> entry : sortedProps.entrySet()) {
addPropertyFingerprint(builder, component, entry.getKey(), entry.getValue());
}
}
}
private void addBundleFingerprint(final StringBuilder builder, final BundleDTO bundle) {
if (bundle != null) {
builder.append(bundle.getGroup());
builder.append(bundle.getArtifact());
builder.append(bundle.getVersion());
} else {
builder.append("MISSING_BUNDLE");
}
}
private BundleCoordinate getCoordinate(final String type, final BundleDTO dto) {
BundleCoordinate coordinate;
try {
coordinate = BundleUtils.getCompatibleBundle(extensionManager, type, dto);
} catch (final IllegalStateException e) {
if (dto == null) {
coordinate = BundleCoordinate.UNKNOWN_COORDINATE;
} else {
coordinate = new BundleCoordinate(dto.getGroup(), dto.getArtifact(), dto.getVersion());
}
}
return coordinate;
}
private void addReportingTaskFingerprint(final StringBuilder builder, final ReportingTaskDTO dto) {
builder.append(dto.getId());
builder.append(dto.getType());
builder.append(dto.getName());
addBundleFingerprint(builder, dto.getBundle());
builder.append(dto.getComments());
builder.append(dto.getSchedulingPeriod());
builder.append(dto.getSchedulingStrategy());
builder.append(dto.getAnnotationData());
// get the temp instance of the ReportingTask so that we know the default property values
final BundleCoordinate coordinate = getCoordinate(dto.getType(), dto.getBundle());
final ConfigurableComponent configurableComponent = extensionManager.getTempComponent(dto.getType(), coordinate);
if (configurableComponent == null) {
logger.warn("Unable to get ReportingTask of type {}; its default properties will be fingerprinted instead of being ignored.", dto.getType());
}
addPropertiesFingerprint(builder, configurableComponent, dto.getProperties());
}
private Comparator<Element> getIdsComparator() {
return new Comparator<Element>() {
@Override
public int compare(final Element e1, final Element e2) {
// compare using processor ids
final String e1Id = getFirstValue(DomUtils.getChildNodesByTagName(e1, "id"));
final String e2Id = getFirstValue(DomUtils.getChildNodesByTagName(e2, "id"));
return e1Id.compareTo(e2Id);
}
};
}
private Comparator<Element> getVariableNameComparator() {
return new Comparator<Element>() {
@Override
public int compare(final Element e1, final Element e2) {
if (e1 == null && e2 == null) {
return 0;
}
if (e1 == null) {
return 1;
}
if (e2 == null) {
return -1;
}
final String varName1 = e1.getAttribute("name");
final String varName2 = e2.getAttribute("name");
return varName1.compareTo(varName2);
}
};
}
private Comparator<Element> getProcessorPropertiesComparator() {
return new Comparator<Element>() {
@Override
public int compare(final Element e1, final Element e2) {
// combine the property name and value for the first required property
final String e1PropName = getFirstValue(DomUtils.getChildNodesByTagName(e1, "name"));
String e1PropValue = getFirstValue(DomUtils.getChildNodesByTagName(e1, "value"));
if (isEncrypted(e1PropValue)) {
e1PropValue = getLoggableRepresentationOfSensitiveValue(e1PropValue);
}
final String e1CombinedValue = e1PropName + e1PropValue;
// combine the property name and value for the second required property
final String e2PropName = getFirstValue(DomUtils.getChildNodesByTagName(e2, "name"));
String e2PropValue = getFirstValue(DomUtils.getChildNodesByTagName(e2, "value"));
if (isEncrypted(e2PropValue)) {
e2PropValue = getLoggableRepresentationOfSensitiveValue(e2PropValue);
}
final String e2CombinedValue = e2PropName + e2PropValue;
// compare the combined values
return e1CombinedValue.compareTo(e2CombinedValue);
}
};
}
private Comparator<Element> getElementTextComparator() {
return new Comparator<Element>() {
@Override
public int compare(final Element e1, final Element e2) {
if (e2 == null) {
return -1;
} else if (e1 == null) {
return 1;
}
return e1.getTextContent().compareTo(e2.getTextContent());
}
};
}
private List<Element> sortElements(final NodeList nodeList, final Comparator<Element> comparator) {
final List<Element> result = new ArrayList<>();
// add node list to sorted list
for (int i = 0; i < nodeList.getLength(); i++) {
result.add((Element) nodeList.item(i));
}
Collections.sort(result, comparator);
return result;
}
private String getValue(final Node node) {
return getValue(node, NO_VALUE);
}
private String getValue(final Node node, final String defaultValue) {
final String value;
if (node.getTextContent() == null || StringUtils.isBlank(node.getTextContent())) {
value = defaultValue;
} else {
value = node.getTextContent().trim();
}
return value;
}
private String getValue(final String value, final String defaultValue) {
if (StringUtils.isBlank(value)) {
return defaultValue;
} else {
return value;
}
}
private String getFirstValue(final NodeList nodeList) {
return getFirstValue(nodeList, NO_VALUE);
}
private String getFirstValue(final NodeList nodeList, final String defaultValue) {
final String value;
if (nodeList == null || nodeList.getLength() == 0) {
value = defaultValue;
} else {
value = getValue(nodeList.item(0));
}
return value;
}
private StringBuilder appendFirstValue(final StringBuilder builder, final NodeList nodeList) {
return appendFirstValue(builder, nodeList, NO_VALUE);
}
private StringBuilder appendFirstValue(final StringBuilder builder, final NodeList nodeList, final String defaultValue) {
return builder.append(getFirstValue(nodeList, defaultValue));
}
private boolean isEncrypted(final String value) {
return (value.startsWith(ENCRYPTED_VALUE_PREFIX) && value.endsWith(ENCRYPTED_VALUE_SUFFIX));
}
private String decrypt(final String value) throws FingerprintException {
final int decryptStartIdx = ENCRYPTED_VALUE_PREFIX.length();
final int decryptEndIdx = value.length() - ENCRYPTED_VALUE_SUFFIX.length();
return encryptor.decrypt(value.substring(decryptStartIdx, decryptEndIdx));
}
}