package com.thinkbiganalytics.nifi.rest.client.layout;
/*-
* #%L
* thinkbig-nifi-rest-client-api
* %%
* Copyright (C) 2017 ThinkBig Analytics
* %%
* Licensed 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.
* #L%
*/
import com.thinkbiganalytics.nifi.rest.client.NiFiRestClient;
import org.apache.nifi.web.api.dto.ConnectionDTO;
import org.apache.nifi.web.api.dto.PortDTO;
import org.apache.nifi.web.api.dto.PositionDTO;
import org.apache.nifi.web.api.dto.ProcessGroupDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Align Nifi Components under a supplied ProcessGroupId
*/
public class AlignProcessGroupComponents {
private static final Logger log = LoggerFactory.getLogger(AlignProcessGroupComponents.class);
NiFiRestClient niFiRestClient;
/**
* Map storing the various LayoutGroups computed for the ProcessGroups within the supplied {@code parentProcessGroupId}
*/
Map<String, LayoutGroup> layoutGroups = new HashMap<>();
/**
* The ProcessGroup to inspect
*/
private String parentProcessGroupId;
/**
* Internal counter as to the number of {@code LayoutGroup}s created
*/
private Integer groupNumber = 0;
/**
* Configuration as to the height, padding for the various components
*/
private AlignComponentsConfig alignmentConfig;
/**
* The Group matching the supplied {@code parentProcessGroupId}
*/
private ProcessGroupDTO parentProcessGroup;
/**
* map of all the groups under the supplied parent
*/
private Map<String, ProcessGroupDTO> processGroupDTOMap;
/**
* map of all the outputs under the supplied parent
*/
private Map<String, PortDTO> outputPortMap = new HashMap<>();
/**
* map of all the inputs under the supplied parent
*/
private Map<String, PortDTO> inputPortMap = new HashMap<>();
/**
* Map of the processGroupId to object with connections
*/
private Map<String, ProcessGroupAndConnections> processGroupWithConnectionsMap = new HashMap<>();
/**
* Flag to indicate when Alignment is complete
*/
private boolean aligned = false;
/**
* Pointer to the last Positioned LayoutGroup
*/
private LayoutGroup lastPositionedGroup;
public AlignProcessGroupComponents(NiFiRestClient niFiRestClient, String parentProcessGroupId, AlignComponentsConfig alignmentConfig) {
this.niFiRestClient = niFiRestClient;
this.parentProcessGroupId = parentProcessGroupId;
this.alignmentConfig = alignmentConfig;
}
public AlignProcessGroupComponents(NiFiRestClient niFiRestClient, String parentProcessGroupId) {
this(niFiRestClient, parentProcessGroupId, new AlignComponentsConfig());
}
/**
* For the passed in {@code parentProcessGroupId} it will group the items and then apply different
*/
public ProcessGroupDTO autoLayout() {
try {
groupItems();
//organize each group of items on the screen
layoutGroups.entrySet().stream().sorted(Map.Entry.<String, LayoutGroup>comparingByKey()).forEachOrdered(entry -> arrangeProcessGroup(entry.getValue()));
aligned = true;
} catch (Exception e) {
log.error("Error Aligning items in Process Group {}. {}", parentProcessGroupId, e.getMessage());
}
return parentProcessGroup;
}
/**
* Group the items together into different {@code LayoutGroup} based upon connections from the ProcessGroup to its various ports.
*/
public Map<String, LayoutGroup> groupItems() {
layoutGroups = new HashMap<>();
//find the parent and children
if (parentProcessGroupId == "root") {
parentProcessGroup = niFiRestClient.processGroups().findRoot();
} else {
parentProcessGroup = niFiRestClient.processGroups().findById(parentProcessGroupId, false, true).orElse(null);
}
final Set<ProcessGroupDTO> children = parentProcessGroup.getContents().getProcessGroups();
processGroupDTOMap = new HashMap<>();
children.stream().forEach(group -> processGroupDTOMap.put(group.getId(), group));
children.stream().forEach(group -> processGroupWithConnectionsMap.put(group.getId(), new ProcessGroupAndConnections(group)));
parentProcessGroup.getContents().getOutputPorts().stream().forEach(portDTO -> outputPortMap.put(portDTO.getId(), portDTO));
parentProcessGroup.getContents().getInputPorts().stream().forEach(portDTO -> inputPortMap.put(portDTO.getId(), portDTO));
//map any ports to processgroups
//group the items by their respective output ports
createLayoutGroups();
return layoutGroups;
}
/**
* Based upon the LayoutGroup apply the correct Rendering technique to layout the components
*/
private void arrangeProcessGroup(LayoutGroup layoutGroup) {
log.info("Arrange Group {}", layoutGroup.getClass().getSimpleName());
//set the starting Y coords for this group
Double start = lastPositionedGroup == null ? 0.0d : lastPositionedGroup.getBottomY() + alignmentConfig.getGroupPadding();
layoutGroup.setTopAndBottom(start, new Double(layoutGroup.getHeight() + start));
layoutGroup.setGroupNumber(groupNumber++);
if (layoutGroup instanceof InputPortToProcessGroup) {
arrangeInputPortToProcessGroupLayout((InputPortToProcessGroup) layoutGroup);
} else if (layoutGroup instanceof ProcessGroupToOutputPort) {
arrangeProcessGroupToOutputPortLayout((ProcessGroupToOutputPort) layoutGroup);
} else if (layoutGroup instanceof ProcessGroupToProcessGroup) {
arrangeProcessGroupLayout((ProcessGroupToProcessGroup) layoutGroup);
} else if (layoutGroup instanceof ProcessGroupWithoutConnections) {
arrangeProcessGroupWithoutConnectionsLayout((ProcessGroupWithoutConnections) layoutGroup);
}
lastPositionedGroup = layoutGroup;
}
public Map<String, ProcessGroupAndConnections> getProcessGroupWithConnectionsMap() {
return processGroupWithConnectionsMap;
}
public boolean isAligned() {
return aligned;
}
private void arrangeProcessGroupWithoutConnectionsLayout(ProcessGroupWithoutConnections layout) {
defaultProcessGroupLayoutArrangement(layout);
}
/**
* Arrange top and bottom
*/
private void arrangeProcessGroupLayout(ProcessGroupToProcessGroup layout) {
//ClockRenderer clock = new ClockRenderer(layoutGroups,alignmentConfig);
//arrange the dest in the center
TopBottomRowsRenderer topBottomRowsRenderer = new TopBottomRowsRenderer(layout, alignmentConfig);
SingleRowRenderer rowRenderer = new SingleRowRenderer(layout, alignmentConfig, layout.getMiddleY());
alignProcessGroups(layout.getDestinations(), rowRenderer);
alignProcessGroups(layout.getProcessGroupDTOs(), topBottomRowsRenderer);
}
private void arrangeProcessGroupToOutputPortLayout(ProcessGroupToOutputPort layout) {
if (!layout.getPorts().isEmpty()) {
Integer outputPortCount = layout.getPorts().size();
ColumnRenderer columnRenderer = new ColumnRenderer(layout, alignmentConfig, (alignmentConfig.getCenterX() - (alignmentConfig.getPortWidth() / 2)), outputPortCount);
columnRenderer
.updateHeight((columnRenderer.getItemCount() * alignmentConfig.getPortHeight() + alignmentConfig.getProcessGroupHeight() + (2 * alignmentConfig.getProcessGroupPaddingTopBottom())));
alignOutputPorts(layout, columnRenderer);
}
SingleRowRenderer rowRenderer = new SingleRowRenderer(layout, alignmentConfig, layout.getMiddleY(alignmentConfig.getProcessGroupHeight() / 2));
alignProcessGroups(layout.getProcessGroupDTOs(), rowRenderer);
}
private void arrangeInputPortToProcessGroupLayout(InputPortToProcessGroup layout) {
if (!layout.getPorts().isEmpty()) {
Integer outputPortCount = layout.getPorts().size();
ColumnRenderer columnRenderer = new ColumnRenderer(layout, alignmentConfig, (alignmentConfig.getCenterX() - (alignmentConfig.getPortWidth() / 2)), outputPortCount);
columnRenderer
.updateHeight((columnRenderer.getItemCount() * alignmentConfig.getPortHeight() + alignmentConfig.getProcessGroupHeight() + (2 * alignmentConfig.getProcessGroupPaddingTopBottom())));
alignInputPorts(layout, columnRenderer);
}
SingleRowRenderer rowRenderer = new SingleRowRenderer(layout, alignmentConfig, layout.getMiddleY(alignmentConfig.getProcessGroupHeight() / 2));
alignProcessGroups(layout.getProcessGroupDTOs(), rowRenderer);
}
private void alignOutputPorts(ProcessGroupToOutputPort layoutGroup, AbstractRenderer renderer) {
layoutGroup.getPorts().values().stream().forEach(port -> {
PortDTO positionPort = new PortDTO();
positionPort.setId(port.getId());
PositionDTO lastPosition = renderer.getLastPosition();
PositionDTO newPosition = renderer.getNextPosition(lastPosition);
positionPort.setPosition(newPosition);
niFiRestClient.ports().updateOutputPort(parentProcessGroupId, positionPort);
log.info("Aligned Port {} at {},{}", port.getName(), positionPort.getPosition().getX(), positionPort.getPosition().getY());
});
}
private void alignInputPorts(InputPortToProcessGroup layoutGroup, AbstractRenderer renderer) {
layoutGroup.getPorts().values().stream().forEach(port -> {
PortDTO positionPort = new PortDTO();
positionPort.setId(port.getId());
PositionDTO lastPosition = renderer.getLastPosition();
PositionDTO newPosition = renderer.getNextPosition(lastPosition);
positionPort.setPosition(newPosition);
niFiRestClient.ports().updateInputPort(parentProcessGroupId, positionPort);
log.info("Aligned Port {} at {},{}", port.getName(), positionPort.getPosition().getX(), positionPort.getPosition().getY());
});
}
private void alignProcessGroups(Set<ProcessGroupDTO> processGroups, AbstractRenderer renderer) {
processGroups.stream().forEach(processGroupDTO -> {
ProcessGroupDTO positionProcessGroup = new ProcessGroupDTO();
positionProcessGroup.setId(processGroupDTO.getId());
PositionDTO lastPosition = renderer.getLastPosition();
PositionDTO newPosition = renderer.getNextPosition(lastPosition);
positionProcessGroup.setPosition(newPosition);
niFiRestClient.processGroups().update(positionProcessGroup);
log.info("Aligned ProcessGroup {} at {},{}", processGroupDTO.getName(), positionProcessGroup.getPosition().getX(), positionProcessGroup.getPosition().getY());
});
}
private void defaultProcessGroupLayoutArrangement(LayoutGroup layoutGroup) {
SingleRowRenderer rowRenderer = new SingleRowRenderer(layoutGroup, alignmentConfig, layoutGroup.getMiddleY(alignmentConfig.getProcessGroupHeight() / 2));
alignProcessGroups(layoutGroup.getProcessGroupDTOs(), rowRenderer);
}
private boolean isGroupToGroupConnection(ConnectionDTO connectionDTO) {
return (processGroupDTOMap.containsKey(connectionDTO.getDestination().getGroupId()) && processGroupDTOMap.containsKey(connectionDTO.getSource().getGroupId()));
}
private boolean isOutputPortToGroupConnection(ConnectionDTO connectionDTO) {
return (outputPortMap.containsKey(connectionDTO.getDestination().getId()) || outputPortMap.containsKey(connectionDTO.getSource().getId()))
&& (processGroupDTOMap.containsKey(connectionDTO.getDestination().getGroupId()) || processGroupDTOMap.containsKey(connectionDTO.getSource().getGroupId()));
}
private boolean isInputPortToGroupConnection(ConnectionDTO connectionDTO) {
return (inputPortMap.containsKey(connectionDTO.getDestination().getId()) || inputPortMap.containsKey(connectionDTO.getSource().getId()))
&& (processGroupDTOMap.containsKey(connectionDTO.getDestination().getGroupId()) || processGroupDTOMap.containsKey(connectionDTO.getSource().getGroupId()));
}
/**
* Group the items together to create the various LayoutGroups needed for different Rendering
*/
private void createLayoutGroups() {
Map<String, Set<ProcessGroupDTO>> outputPortIdToGroup = new HashMap<String, Set<ProcessGroupDTO>>();
Map<String, Set<PortDTO>> groupIdToOutputPorts = new HashMap<>();
Map<String, Set<ProcessGroupDTO>> inputPortIdToGroup = new HashMap<String, Set<ProcessGroupDTO>>();
Map<String, Set<PortDTO>> groupIdToInputPorts = new HashMap<>();
Map<String, Set<String>> groupIdToGroup = new HashMap<>();
parentProcessGroup.getContents().getConnections().stream().filter(
connectionDTO -> (isOutputPortToGroupConnection(connectionDTO) || isGroupToGroupConnection(connectionDTO) || isInputPortToGroupConnection(connectionDTO)))
.forEach(connectionDTO -> {
PortDTO
outputPort =
outputPortMap.get(connectionDTO.getDestination().getId()) == null ? outputPortMap.get(connectionDTO.getSource().getId())
: outputPortMap.get(connectionDTO.getDestination().getId());
PortDTO
inputPort =
inputPortMap.get(connectionDTO.getSource().getId()) == null ? inputPortMap.get(connectionDTO.getDestination().getId())
: inputPortMap.get(connectionDTO.getSource().getId());
ProcessGroupDTO
destinationGroup =
processGroupDTOMap.get(connectionDTO.getDestination().getGroupId());
ProcessGroupDTO
sourceGroup =
processGroupDTOMap.get(connectionDTO.getSource().getGroupId());
if (outputPort != null) {
ProcessGroupDTO processGroup = destinationGroup == null ? sourceGroup : destinationGroup;
outputPortIdToGroup.computeIfAbsent(outputPort.getId(), (key) -> new HashSet<ProcessGroupDTO>()).add(processGroup);
groupIdToOutputPorts.computeIfAbsent(processGroup.getId(), (key) -> new HashSet<PortDTO>()).add(outputPort);
if (processGroupWithConnectionsMap.containsKey(processGroup.getId())) {
processGroupWithConnectionsMap.get(processGroup.getId()).addConnection(connectionDTO).addPort(outputPort);
}
}
if (inputPort != null) {
ProcessGroupDTO processGroup = destinationGroup == null ? sourceGroup : destinationGroup;
inputPortIdToGroup.computeIfAbsent(inputPort.getId(), (key) -> new HashSet<ProcessGroupDTO>()).add(processGroup);
groupIdToInputPorts.computeIfAbsent(processGroup.getId(), (key) -> new HashSet<PortDTO>()).add(inputPort);
if (processGroupWithConnectionsMap.containsKey(processGroup.getId())) {
processGroupWithConnectionsMap.get(processGroup.getId()).addConnection(connectionDTO).addPort(outputPort);
}
} else if (destinationGroup != null && sourceGroup != null) {
groupIdToGroup.computeIfAbsent(sourceGroup.getId(), (key) -> new HashSet<String>()).add(destinationGroup.getId());
}
});
// group port connections together
groupIdToOutputPorts.entrySet().stream().forEach(entry -> {
String processGroupId = entry.getKey();
String portKey = entry.getValue().stream().map(portDTO -> portDTO.getId()).sorted().collect(Collectors.joining(","));
portKey = "AAA" + portKey;
layoutGroups.computeIfAbsent(portKey, (key) -> new ProcessGroupToOutputPort(entry.getValue())).add(processGroupDTOMap.get(processGroupId));
});
// group port connections together
groupIdToInputPorts.entrySet().stream().forEach(entry -> {
String processGroupId = entry.getKey();
String portKey = entry.getValue().stream().map(portDTO -> portDTO.getId()).sorted().collect(Collectors.joining(","));
portKey = "BBB" + portKey;
layoutGroups.computeIfAbsent(portKey, (key) -> new InputPortToProcessGroup(entry.getValue())).add(processGroupDTOMap.get(processGroupId));
});
groupIdToGroup.entrySet().stream().forEach(entry -> {
String sourceGroupId = entry.getKey();
String processGroupKey = entry.getValue().stream().sorted().collect(Collectors.joining(","));
processGroupKey = "CCC" + processGroupKey;
layoutGroups.computeIfAbsent(processGroupKey, (key) -> new ProcessGroupToProcessGroup(entry.getValue())).add(processGroupDTOMap.get(entry.getKey()));
});
//add in any groups that dont have connections to ports
processGroupDTOMap.values().stream().filter(
processGroupDTO -> !groupIdToGroup.values().stream().flatMap(set -> set.stream()).collect(Collectors.toSet()).contains(processGroupDTO.getId()) && !groupIdToInputPorts
.containsKey(processGroupDTO.getId()) && !groupIdToOutputPorts.containsKey(processGroupDTO.getId()) && !groupIdToGroup.containsKey(processGroupDTO.getId()))
.forEach(group -> {
layoutGroups.computeIfAbsent("NO_PORTS", (key) -> new ProcessGroupWithoutConnections()).add(group);
});
}
public void setNiFiRestClient(NiFiRestClient niFiRestClient) {
this.niFiRestClient = niFiRestClient;
}
/**
* Layout where a ProcessGroup has no Connections
*/
public class ProcessGroupWithoutConnections extends LayoutGroup {
public ProcessGroupWithoutConnections() {
}
public Integer calculateHeight() {
return (alignmentConfig.getProcessGroupHeight()) + (alignmentConfig.getProcessGroupPaddingTopBottom());
}
}
/**
* Layout Group where a ProcessGroup is connected directly to another ProcessGroup
*/
public class ProcessGroupToProcessGroup extends LayoutGroup {
private Set<ProcessGroupDTO> destinations = new HashSet<>();
public ProcessGroupToProcessGroup(Set<String> destinations) {
this.destinations = destinations.stream().map(groupId -> processGroupDTOMap.get(groupId)).collect(Collectors.toSet());
}
public Set<ProcessGroupDTO> getSources() {
return super.getProcessGroupDTOs();
}
@Override
public Integer calculateHeight() {
return alignmentConfig.getProcessGroupHeight() + alignmentConfig.getProcessGroupPaddingTopBottom();
}
public Set<ProcessGroupDTO> getDestinations() {
return destinations;
}
}
/**
* Layout Group where an Input Port is connected to a ProcessGroup
*/
public class InputPortToProcessGroup extends ProcessGroupToOutputPort {
public InputPortToProcessGroup(Set<PortDTO> inputPorts) {
super(inputPorts);
}
}
/**
* Group where a ProcessGroup is connected to an OutputPort
*/
public class ProcessGroupToOutputPort extends ProcessGroupToPort {
public ProcessGroupToOutputPort(Set<PortDTO> outputPorts) {
super(outputPorts);
}
}
/**
* LayoutGroup where a Port is connected to a Process Group
*/
public abstract class ProcessGroupToPort extends LayoutGroup {
private Map<String, PortDTO> ports = new HashMap<>();
public ProcessGroupToPort(Set<PortDTO> ports) {
if (ports != null) {
ports.stream().forEach(portDTO -> {
this.ports.put(portDTO.getId(), portDTO);
});
}
}
public Integer calculateHeight() {
Integer portCount = ports.size();
return (alignmentConfig.getPortHeight() * portCount) + alignmentConfig.getProcessGroupHeight() + (alignmentConfig.getProcessGroupPaddingTopBottom() * portCount);
}
public Double getMiddleY() {
return super.getMiddleY();
}
public Map<String, PortDTO> getPorts() {
return ports;
}
}
}