/*
* 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.serialization;
import org.apache.nifi.bundle.BundleCoordinate;
import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.connectable.ConnectableType;
import org.apache.nifi.connectable.Connection;
import org.apache.nifi.connectable.Funnel;
import org.apache.nifi.connectable.Port;
import org.apache.nifi.connectable.Position;
import org.apache.nifi.connectable.Size;
import org.apache.nifi.controller.FlowController;
import org.apache.nifi.controller.ProcessorNode;
import org.apache.nifi.controller.ReportingTaskNode;
import org.apache.nifi.controller.Template;
import org.apache.nifi.controller.label.Label;
import org.apache.nifi.controller.service.ControllerServiceNode;
import org.apache.nifi.controller.service.ControllerServiceState;
import org.apache.nifi.encrypt.StringEncryptor;
import org.apache.nifi.flowfile.FlowFilePrioritizer;
import org.apache.nifi.groups.ProcessGroup;
import org.apache.nifi.groups.RemoteProcessGroup;
import org.apache.nifi.persistence.TemplateSerializer;
import org.apache.nifi.processor.Relationship;
import org.apache.nifi.remote.RemoteGroupPort;
import org.apache.nifi.remote.RootGroupPort;
import org.apache.nifi.util.CharacterFilterUtils;
import org.apache.nifi.util.StringUtils;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* Serializes a Flow Controller as XML to an output stream.
*
* NOT THREAD-SAFE.
*/
public class StandardFlowSerializer implements FlowSerializer {
private static final String MAX_ENCODING_VERSION = "1.1";
private final StringEncryptor encryptor;
public StandardFlowSerializer(final StringEncryptor encryptor) {
this.encryptor = encryptor;
}
@Override
public void serialize(final FlowController controller, final OutputStream os, final ScheduledStateLookup scheduledStateLookup) throws FlowSerializationException {
try {
// create a new, empty document
final DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
docFactory.setNamespaceAware(true);
final DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
final Document doc = docBuilder.newDocument();
// populate document with controller state
final Element rootNode = doc.createElement("flowController");
rootNode.setAttribute("encoding-version", MAX_ENCODING_VERSION);
doc.appendChild(rootNode);
addTextElement(rootNode, "maxTimerDrivenThreadCount", controller.getMaxTimerDrivenThreadCount());
addTextElement(rootNode, "maxEventDrivenThreadCount", controller.getMaxEventDrivenThreadCount());
addProcessGroup(rootNode, controller.getGroup(controller.getRootGroupId()), "rootGroup", scheduledStateLookup);
// Add root-level controller services
final Element controllerServicesNode = doc.createElement("controllerServices");
rootNode.appendChild(controllerServicesNode);
for (final ControllerServiceNode serviceNode : controller.getRootControllerServices()) {
addControllerService(controllerServicesNode, serviceNode);
}
final Element reportingTasksNode = doc.createElement("reportingTasks");
rootNode.appendChild(reportingTasksNode);
for (final ReportingTaskNode taskNode : controller.getAllReportingTasks()) {
addReportingTask(reportingTasksNode, taskNode, encryptor);
}
final DOMSource domSource = new DOMSource(doc);
final StreamResult streamResult = new StreamResult(new BufferedOutputStream(os));
// configure the transformer and convert the DOM
final TransformerFactory transformFactory = TransformerFactory.newInstance();
final Transformer transformer = transformFactory.newTransformer();
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
// transform the document to byte stream
transformer.transform(domSource, streamResult);
} catch (final ParserConfigurationException | DOMException | TransformerFactoryConfigurationError | IllegalArgumentException | TransformerException e) {
throw new FlowSerializationException(e);
}
}
private void addSize(final Element parentElement, final Size size) {
final Element element = parentElement.getOwnerDocument().createElement("size");
element.setAttribute("width", String.valueOf(size.getWidth()));
element.setAttribute("height", String.valueOf(size.getHeight()));
parentElement.appendChild(element);
}
private void addPosition(final Element parentElement, final Position position) {
addPosition(parentElement, position, "position");
}
private void addPosition(final Element parentElement, final Position position, final String elementName) {
final Element element = parentElement.getOwnerDocument().createElement(elementName);
element.setAttribute("x", String.valueOf(position.getX()));
element.setAttribute("y", String.valueOf(position.getY()));
parentElement.appendChild(element);
}
private void addProcessGroup(final Element parentElement, final ProcessGroup group, final String elementName, final ScheduledStateLookup scheduledStateLookup) {
final Document doc = parentElement.getOwnerDocument();
final Element element = doc.createElement(elementName);
parentElement.appendChild(element);
addTextElement(element, "id", group.getIdentifier());
addTextElement(element, "name", group.getName());
addPosition(element, group.getPosition());
addTextElement(element, "comment", group.getComments());
for (final ProcessorNode processor : group.getProcessors()) {
addProcessor(element, processor, scheduledStateLookup);
}
if (group.isRootGroup()) {
for (final Port port : group.getInputPorts()) {
addRootGroupPort(element, (RootGroupPort) port, "inputPort");
}
for (final Port port : group.getOutputPorts()) {
addRootGroupPort(element, (RootGroupPort) port, "outputPort");
}
} else {
for (final Port port : group.getInputPorts()) {
addPort(element, port, "inputPort");
}
for (final Port port : group.getOutputPorts()) {
addPort(element, port, "outputPort");
}
}
for (final Label label : group.getLabels()) {
addLabel(element, label);
}
for (final Funnel funnel : group.getFunnels()) {
addFunnel(element, funnel);
}
for (final ProcessGroup childGroup : group.getProcessGroups()) {
addProcessGroup(element, childGroup, "processGroup", scheduledStateLookup);
}
for (final RemoteProcessGroup remoteRef : group.getRemoteProcessGroups()) {
addRemoteProcessGroup(element, remoteRef);
}
for (final Connection connection : group.getConnections()) {
addConnection(element, connection);
}
for (final ControllerServiceNode service : group.getControllerServices(false)) {
addControllerService(element, service);
}
for (final Template template : group.getTemplates()) {
addTemplate(element, template);
}
}
private static void addBundle(final Element parentElement, final BundleCoordinate coordinate) {
// group
final Element groupElement = parentElement.getOwnerDocument().createElement("group");
groupElement.setTextContent(coordinate.getGroup());
// artifact
final Element artifactElement = parentElement.getOwnerDocument().createElement("artifact");
artifactElement.setTextContent(coordinate.getId());
// version
final Element versionElement = parentElement.getOwnerDocument().createElement("version");
versionElement.setTextContent(coordinate.getVersion());
// bundle
final Element bundleElement = parentElement.getOwnerDocument().createElement("bundle");
bundleElement.appendChild(groupElement);
bundleElement.appendChild(artifactElement);
bundleElement.appendChild(versionElement);
parentElement.appendChild(bundleElement);
}
private void addStyle(final Element parentElement, final Map<String, String> style) {
final Element element = parentElement.getOwnerDocument().createElement("styles");
for (final Map.Entry<String, String> entry : style.entrySet()) {
final Element styleElement = parentElement.getOwnerDocument().createElement("style");
styleElement.setAttribute("name", entry.getKey());
styleElement.setTextContent(entry.getValue());
element.appendChild(styleElement);
}
parentElement.appendChild(element);
}
private void addLabel(final Element parentElement, final Label label) {
final Document doc = parentElement.getOwnerDocument();
final Element element = doc.createElement("label");
parentElement.appendChild(element);
addTextElement(element, "id", label.getIdentifier());
addPosition(element, label.getPosition());
addSize(element, label.getSize());
addStyle(element, label.getStyle());
addTextElement(element, "value", label.getValue());
parentElement.appendChild(element);
}
private void addFunnel(final Element parentElement, final Funnel funnel) {
final Document doc = parentElement.getOwnerDocument();
final Element element = doc.createElement("funnel");
parentElement.appendChild(element);
addTextElement(element, "id", funnel.getIdentifier());
addPosition(element, funnel.getPosition());
}
private void addRemoteProcessGroup(final Element parentElement, final RemoteProcessGroup remoteRef) {
final Document doc = parentElement.getOwnerDocument();
final Element element = doc.createElement("remoteProcessGroup");
parentElement.appendChild(element);
addTextElement(element, "id", remoteRef.getIdentifier());
addTextElement(element, "name", remoteRef.getName());
addPosition(element, remoteRef.getPosition());
addTextElement(element, "comment", remoteRef.getComments());
addTextElement(element, "url", remoteRef.getTargetUri());
addTextElement(element, "urls", remoteRef.getTargetUris());
addTextElement(element, "timeout", remoteRef.getCommunicationsTimeout());
addTextElement(element, "yieldPeriod", remoteRef.getYieldDuration());
addTextElement(element, "transmitting", String.valueOf(remoteRef.isTransmitting()));
addTextElement(element, "transportProtocol", remoteRef.getTransportProtocol().name());
addTextElement(element, "proxyHost", remoteRef.getProxyHost());
if (remoteRef.getProxyPort() != null) {
addTextElement(element, "proxyPort", remoteRef.getProxyPort());
}
addTextElement(element, "proxyUser", remoteRef.getProxyUser());
if (!StringUtils.isEmpty(remoteRef.getProxyPassword())) {
final String value = ENC_PREFIX + encryptor.encrypt(remoteRef.getProxyPassword()) + ENC_SUFFIX;
addTextElement(element, "proxyPassword", value);
}
if (remoteRef.getNetworkInterface() != null) {
addTextElement(element, "networkInterface", remoteRef.getNetworkInterface());
}
for (final RemoteGroupPort port : remoteRef.getInputPorts()) {
if (port.hasIncomingConnection()) {
addRemoteGroupPort(element, port, "inputPort");
}
}
for (final RemoteGroupPort port : remoteRef.getOutputPorts()) {
if (!port.getConnections().isEmpty()) {
addRemoteGroupPort(element, port, "outputPort");
}
}
parentElement.appendChild(element);
}
private void addRemoteGroupPort(final Element parentElement, final RemoteGroupPort port, final String elementName) {
final Document doc = parentElement.getOwnerDocument();
final Element element = doc.createElement(elementName);
parentElement.appendChild(element);
addTextElement(element, "id", port.getIdentifier());
addTextElement(element, "name", port.getName());
addPosition(element, port.getPosition());
addTextElement(element, "comments", port.getComments());
addTextElement(element, "scheduledState", port.getScheduledState().name());
addTextElement(element, "maxConcurrentTasks", port.getMaxConcurrentTasks());
addTextElement(element, "useCompression", String.valueOf(port.isUseCompression()));
final Integer batchCount = port.getBatchCount();
if (batchCount != null && batchCount > 0) {
addTextElement(element, "batchCount", batchCount);
}
final String batchSize = port.getBatchSize();
if (batchSize != null && batchSize.length() > 0) {
addTextElement(element, "batchSize", batchSize);
}
final String batchDuration = port.getBatchDuration();
if (batchDuration != null && batchDuration.length() > 0) {
addTextElement(element, "batchDuration", batchDuration);
}
parentElement.appendChild(element);
}
private void addPort(final Element parentElement, final Port port, final String elementName) {
final Document doc = parentElement.getOwnerDocument();
final Element element = doc.createElement(elementName);
parentElement.appendChild(element);
addTextElement(element, "id", port.getIdentifier());
addTextElement(element, "name", port.getName());
addPosition(element, port.getPosition());
addTextElement(element, "comments", port.getComments());
addTextElement(element, "scheduledState", port.getScheduledState().name());
parentElement.appendChild(element);
}
private void addRootGroupPort(final Element parentElement, final RootGroupPort port, final String elementName) {
final Document doc = parentElement.getOwnerDocument();
final Element element = doc.createElement(elementName);
parentElement.appendChild(element);
addTextElement(element, "id", port.getIdentifier());
addTextElement(element, "name", port.getName());
addPosition(element, port.getPosition());
addTextElement(element, "comments", port.getComments());
addTextElement(element, "scheduledState", port.getScheduledState().name());
addTextElement(element, "maxConcurrentTasks", String.valueOf(port.getMaxConcurrentTasks()));
for (final String user : port.getUserAccessControl()) {
addTextElement(element, "userAccessControl", user);
}
for (final String group : port.getGroupAccessControl()) {
addTextElement(element, "groupAccessControl", group);
}
parentElement.appendChild(element);
}
private void addProcessor(final Element parentElement, final ProcessorNode processor, final ScheduledStateLookup scheduledStateLookup) {
final Document doc = parentElement.getOwnerDocument();
final Element element = doc.createElement("processor");
parentElement.appendChild(element);
addTextElement(element, "id", processor.getIdentifier());
addTextElement(element, "name", processor.getName());
addPosition(element, processor.getPosition());
addStyle(element, processor.getStyle());
addTextElement(element, "comment", processor.getComments());
addTextElement(element, "class", processor.getCanonicalClassName());
addBundle(element, processor.getBundleCoordinate());
addTextElement(element, "maxConcurrentTasks", processor.getMaxConcurrentTasks());
addTextElement(element, "schedulingPeriod", processor.getSchedulingPeriod());
addTextElement(element, "penalizationPeriod", processor.getPenalizationPeriod());
addTextElement(element, "yieldPeriod", processor.getYieldPeriod());
addTextElement(element, "bulletinLevel", processor.getBulletinLevel().toString());
addTextElement(element, "lossTolerant", String.valueOf(processor.isLossTolerant()));
addTextElement(element, "scheduledState", scheduledStateLookup.getScheduledState(processor).name());
addTextElement(element, "schedulingStrategy", processor.getSchedulingStrategy().name());
addTextElement(element, "executionNode", processor.getExecutionNode().name());
addTextElement(element, "runDurationNanos", processor.getRunDuration(TimeUnit.NANOSECONDS));
addConfiguration(element, processor.getProperties(), processor.getAnnotationData(), encryptor);
for (final Relationship rel : processor.getAutoTerminatedRelationships()) {
addTextElement(element, "autoTerminatedRelationship", rel.getName());
}
}
private static void addConfiguration(final Element element, final Map<PropertyDescriptor, String> properties, final String annotationData, final StringEncryptor encryptor) {
final Document doc = element.getOwnerDocument();
for (final Map.Entry<PropertyDescriptor, String> entry : properties.entrySet()) {
final PropertyDescriptor descriptor = entry.getKey();
String value = entry.getValue();
if (value != null && descriptor.isSensitive()) {
value = ENC_PREFIX + encryptor.encrypt(value) + ENC_SUFFIX;
}
if (value == null) {
value = descriptor.getDefaultValue();
}
final Element propElement = doc.createElement("property");
addTextElement(propElement, "name", descriptor.getName());
if (value != null) {
addTextElement(propElement, "value", value);
}
element.appendChild(propElement);
}
if (annotationData != null) {
addTextElement(element, "annotationData", annotationData);
}
}
private void addConnection(final Element parentElement, final Connection connection) {
final Document doc = parentElement.getOwnerDocument();
final Element element = doc.createElement("connection");
parentElement.appendChild(element);
addTextElement(element, "id", connection.getIdentifier());
addTextElement(element, "name", connection.getName());
final Element bendPointsElement = doc.createElement("bendPoints");
element.appendChild(bendPointsElement);
for (final Position bendPoint : connection.getBendPoints()) {
addPosition(bendPointsElement, bendPoint, "bendPoint");
}
addTextElement(element, "labelIndex", connection.getLabelIndex());
addTextElement(element, "zIndex", connection.getZIndex());
final String sourceId = connection.getSource().getIdentifier();
final ConnectableType sourceType = connection.getSource().getConnectableType();
final String sourceGroupId;
if (sourceType == ConnectableType.REMOTE_OUTPUT_PORT) {
sourceGroupId = ((RemoteGroupPort) connection.getSource()).getRemoteProcessGroup().getIdentifier();
} else {
sourceGroupId = connection.getSource().getProcessGroup().getIdentifier();
}
final ConnectableType destinationType = connection.getDestination().getConnectableType();
final String destinationId = connection.getDestination().getIdentifier();
final String destinationGroupId;
if (destinationType == ConnectableType.REMOTE_INPUT_PORT) {
destinationGroupId = ((RemoteGroupPort) connection.getDestination()).getRemoteProcessGroup().getIdentifier();
} else {
destinationGroupId = connection.getDestination().getProcessGroup().getIdentifier();
}
addTextElement(element, "sourceId", sourceId);
addTextElement(element, "sourceGroupId", sourceGroupId);
addTextElement(element, "sourceType", sourceType.toString());
addTextElement(element, "destinationId", destinationId);
addTextElement(element, "destinationGroupId", destinationGroupId);
addTextElement(element, "destinationType", destinationType.toString());
for (final Relationship relationship : connection.getRelationships()) {
addTextElement(element, "relationship", relationship.getName());
}
addTextElement(element, "maxWorkQueueSize", connection.getFlowFileQueue().getBackPressureObjectThreshold());
addTextElement(element, "maxWorkQueueDataSize", connection.getFlowFileQueue().getBackPressureDataSizeThreshold());
addTextElement(element, "flowFileExpiration", connection.getFlowFileQueue().getFlowFileExpiration());
for (final FlowFilePrioritizer comparator : connection.getFlowFileQueue().getPriorities()) {
final String className = comparator.getClass().getCanonicalName();
addTextElement(element, "queuePrioritizerClass", className);
}
parentElement.appendChild(element);
}
public void addControllerService(final Element element, final ControllerServiceNode serviceNode) {
final Element serviceElement = element.getOwnerDocument().createElement("controllerService");
addTextElement(serviceElement, "id", serviceNode.getIdentifier());
addTextElement(serviceElement, "name", serviceNode.getName());
addTextElement(serviceElement, "comment", serviceNode.getComments());
addTextElement(serviceElement, "class", serviceNode.getCanonicalClassName());
addBundle(serviceElement, serviceNode.getBundleCoordinate());
final ControllerServiceState state = serviceNode.getState();
final boolean enabled = (state == ControllerServiceState.ENABLED || state == ControllerServiceState.ENABLING);
addTextElement(serviceElement, "enabled", String.valueOf(enabled));
addConfiguration(serviceElement, serviceNode.getProperties(), serviceNode.getAnnotationData(), encryptor);
element.appendChild(serviceElement);
}
public static void addReportingTask(final Element element, final ReportingTaskNode taskNode, final StringEncryptor encryptor) {
final Element taskElement = element.getOwnerDocument().createElement("reportingTask");
addTextElement(taskElement, "id", taskNode.getIdentifier());
addTextElement(taskElement, "name", taskNode.getName());
addTextElement(taskElement, "comment", taskNode.getComments());
addTextElement(taskElement, "class", taskNode.getCanonicalClassName());
addBundle(taskElement, taskNode.getBundleCoordinate());
addTextElement(taskElement, "schedulingPeriod", taskNode.getSchedulingPeriod());
addTextElement(taskElement, "scheduledState", taskNode.getScheduledState().name());
addTextElement(taskElement, "schedulingStrategy", taskNode.getSchedulingStrategy().name());
addConfiguration(taskElement, taskNode.getProperties(), taskNode.getAnnotationData(), encryptor);
element.appendChild(taskElement);
}
private static void addTextElement(final Element element, final String name, final long value) {
addTextElement(element, name, String.valueOf(value));
}
private static void addTextElement(final Element element, final String name, final String value) {
final Document doc = element.getOwnerDocument();
final Element toAdd = doc.createElement(name);
toAdd.setTextContent(CharacterFilterUtils.filterInvalidXmlCharacters(value)); // value should already be filtered, but just in case ensure there are no invalid xml characters
element.appendChild(toAdd);
}
public static void addTemplate(final Element element, final Template template) {
try {
final byte[] serialized = TemplateSerializer.serialize(template.getDetails());
final DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
final DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
final Document document;
try (final InputStream in = new ByteArrayInputStream(serialized)) {
document = docBuilder.parse(in);
}
final Node templateNode = element.getOwnerDocument().importNode(document.getDocumentElement(), true);
element.appendChild(templateNode);
} catch (final Exception e) {
throw new FlowSerializationException(e);
}
}
}