From 91e98aa50b27800d79c2f9db9a226237b87e65cb Mon Sep 17 00:00:00 2001 From: yuri1969 <1969yuri1969@gmail.com> Date: Fri, 22 Dec 2017 20:18:20 +0100 Subject: [PATCH] NIFI-4538 - Add Process Group information to... ...Search results * Separated the search functionality. * Added a unit test. * Added the PG info to UI (a mere draft). * Introduce the nearest versioned group * Removed the top level group results in favour of the nearest versioned group. * This closes #2364 --- .../dto/search/ComponentSearchResultDTO.java | 30 + .../api/dto/search/SearchResultGroupDTO.java | 59 ++ .../nifi/web/controller/ControllerFacade.java | 415 +------------ .../controller/ControllerSearchService.java | 543 ++++++++++++++++++ .../main/resources/nifi-web-api-context.xml | 6 + .../ControllerSearchServiceTest.java | 405 +++++++++++++ .../nf-ng-canvas-flow-status-controller.js | 21 +- 7 files changed, 1069 insertions(+), 410 deletions(-) create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/search/SearchResultGroupDTO.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerSearchService.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/controller/ControllerSearchServiceTest.java diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/search/ComponentSearchResultDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/search/ComponentSearchResultDTO.java index 7cddd7e8a4..ab46ad820c 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/search/ComponentSearchResultDTO.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/search/ComponentSearchResultDTO.java @@ -29,6 +29,8 @@ public class ComponentSearchResultDTO { private String id; private String groupId; + private SearchResultGroupDTO parentGroup; + private SearchResultGroupDTO versionedGroup; private String name; private List matches; @@ -60,6 +62,34 @@ public class ComponentSearchResultDTO { this.groupId = groupId; } + /** + * @return parent group of the component that matched + */ + @ApiModelProperty( + value = "The parent group of the component that matched the search." + ) + public SearchResultGroupDTO getParentGroup() { + return parentGroup; + } + + public void setParentGroup(final SearchResultGroupDTO parentGroup) { + this.parentGroup = parentGroup; + } + + /** + * @return the nearest versioned ancestor group of the component that matched + */ + @ApiModelProperty( + value = "The nearest versioned ancestor group of the component that matched the search." + ) + public SearchResultGroupDTO getVersionedGroup() { + return versionedGroup; + } + + public void setVersionedGroup(final SearchResultGroupDTO versionedGroup) { + this.versionedGroup = versionedGroup; + } + /** * @return name of the component that matched */ diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/search/SearchResultGroupDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/search/SearchResultGroupDTO.java new file mode 100644 index 0000000000..534cc8b503 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/search/SearchResultGroupDTO.java @@ -0,0 +1,59 @@ +/* + * 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.web.api.dto.search; + +import io.swagger.annotations.ApiModelProperty; + +import javax.xml.bind.annotation.XmlType; + +/** + * The result's group level of a performed search. + */ +@XmlType(name = "searchResultGroup") +public class SearchResultGroupDTO { + private String id; + private String name; + + /** + * @return id of this group + */ + @ApiModelProperty( + value = "The id of the group.", + required = true + ) + public String getId() { + return id; + } + + public void setId(final String id) { + this.id = id; + } + + /** + * @return name of this group + */ + @ApiModelProperty( + value = "The name of the group." + ) + public String getName() { + return name; + } + + public void setName(final String name) { + this.name = name; + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java index 6cef841515..72702a6c49 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java @@ -33,10 +33,8 @@ import org.apache.nifi.bundle.Bundle; import org.apache.nifi.bundle.BundleCoordinate; import org.apache.nifi.cluster.protocol.NodeIdentifier; import org.apache.nifi.components.ConfigurableComponent; -import org.apache.nifi.components.PropertyDescriptor; import org.apache.nifi.connectable.Connectable; import org.apache.nifi.connectable.Connection; -import org.apache.nifi.connectable.Funnel; import org.apache.nifi.connectable.Port; import org.apache.nifi.controller.ContentAvailability; import org.apache.nifi.controller.ControllerService; @@ -44,10 +42,8 @@ import org.apache.nifi.controller.Counter; import org.apache.nifi.controller.FlowController; import org.apache.nifi.controller.ProcessorNode; import org.apache.nifi.controller.ReportingTaskNode; -import org.apache.nifi.controller.ScheduledState; import org.apache.nifi.controller.Template; import org.apache.nifi.controller.label.Label; -import org.apache.nifi.controller.queue.FlowFileQueue; import org.apache.nifi.controller.queue.QueueSize; import org.apache.nifi.controller.repository.ContentNotFoundException; import org.apache.nifi.controller.repository.claim.ContentDirection; @@ -66,8 +62,6 @@ import org.apache.nifi.groups.ProcessGroup; import org.apache.nifi.groups.ProcessGroupCounts; import org.apache.nifi.groups.RemoteProcessGroup; import org.apache.nifi.nar.ExtensionManager; -import org.apache.nifi.nar.NarCloseable; -import org.apache.nifi.processor.DataUnit; import org.apache.nifi.processor.Processor; import org.apache.nifi.processor.Relationship; import org.apache.nifi.provenance.ProvenanceEventRecord; @@ -80,18 +74,11 @@ import org.apache.nifi.provenance.search.QuerySubmission; import org.apache.nifi.provenance.search.SearchTerm; import org.apache.nifi.provenance.search.SearchTerms; import org.apache.nifi.provenance.search.SearchableField; -import org.apache.nifi.registry.ComponentVariableRegistry; -import org.apache.nifi.registry.VariableDescriptor; import org.apache.nifi.registry.VariableRegistry; import org.apache.nifi.registry.flow.VersionedProcessGroup; import org.apache.nifi.remote.RemoteGroupPort; import org.apache.nifi.remote.RootGroupPort; import org.apache.nifi.reporting.ReportingTask; -import org.apache.nifi.scheduling.ExecutionNode; -import org.apache.nifi.scheduling.SchedulingStrategy; -import org.apache.nifi.search.SearchContext; -import org.apache.nifi.search.SearchResult; -import org.apache.nifi.search.Searchable; import org.apache.nifi.services.FlowService; import org.apache.nifi.util.BundleUtils; import org.apache.nifi.util.FormatUtils; @@ -112,7 +99,6 @@ import org.apache.nifi.web.api.dto.provenance.ProvenanceSearchableFieldDTO; import org.apache.nifi.web.api.dto.provenance.lineage.LineageDTO; import org.apache.nifi.web.api.dto.provenance.lineage.LineageRequestDTO; import org.apache.nifi.web.api.dto.provenance.lineage.LineageRequestDTO.LineageRequestType; -import org.apache.nifi.web.api.dto.search.ComponentSearchResultDTO; import org.apache.nifi.web.api.dto.search.SearchResultsDTO; import org.apache.nifi.web.api.dto.status.ControllerStatusDTO; import org.apache.nifi.web.api.dto.status.StatusHistoryDTO; @@ -156,6 +142,7 @@ public class ControllerFacade implements Authorizable { private NiFiProperties properties; private DtoFactory dtoFactory; private VariableRegistry variableRegistry; + private ControllerSearchService controllerSearchService; /** * Returns the group id that contains the specified processor. @@ -1524,409 +1511,17 @@ public class ControllerFacade implements Authorizable { */ public SearchResultsDTO search(final String search) { final ProcessGroup rootGroup = flowController.getGroup(flowController.getRootGroupId()); - final SearchResultsDTO results = new SearchResultsDTO(); - search(results, search, rootGroup); + + controllerSearchService.search(results, search, rootGroup); return results; } - private void search(final SearchResultsDTO results, final String search, final ProcessGroup group) { - final NiFiUser user = NiFiUserUtils.getNiFiUser(); - - if (group.isAuthorized(authorizer, RequestAction.READ, user)) { - final ComponentSearchResultDTO groupMatch = search(search, group); - if (groupMatch != null) { - results.getProcessGroupResults().add(groupMatch); - } - } - - for (final ProcessorNode procNode : group.getProcessors()) { - if (procNode.isAuthorized(authorizer, RequestAction.READ, user)) { - final ComponentSearchResultDTO match = search(search, procNode); - if (match != null) { - match.setGroupId(group.getIdentifier()); - results.getProcessorResults().add(match); - } - } - } - - for (final Connection connection : group.getConnections()) { - if (connection.isAuthorized(authorizer, RequestAction.READ, user)) { - final ComponentSearchResultDTO match = search(search, connection); - if (match != null) { - match.setGroupId(group.getIdentifier()); - results.getConnectionResults().add(match); - } - } - } - - for (final RemoteProcessGroup remoteGroup : group.getRemoteProcessGroups()) { - if (remoteGroup.isAuthorized(authorizer, RequestAction.READ, user)) { - final ComponentSearchResultDTO match = search(search, remoteGroup); - if (match != null) { - match.setGroupId(group.getIdentifier()); - results.getRemoteProcessGroupResults().add(match); - } - } - } - - for (final Port port : group.getInputPorts()) { - if (port.isAuthorized(authorizer, RequestAction.READ, user)) { - final ComponentSearchResultDTO match = search(search, port); - if (match != null) { - match.setGroupId(group.getIdentifier()); - results.getInputPortResults().add(match); - } - } - } - - for (final Port port : group.getOutputPorts()) { - if (port.isAuthorized(authorizer, RequestAction.READ, user)) { - final ComponentSearchResultDTO match = search(search, port); - if (match != null) { - match.setGroupId(group.getIdentifier()); - results.getOutputPortResults().add(match); - } - } - } - - for (final Funnel funnel : group.getFunnels()) { - if (funnel.isAuthorized(authorizer, RequestAction.READ, user)) { - final ComponentSearchResultDTO match = search(search, funnel); - if (match != null) { - match.setGroupId(group.getIdentifier()); - results.getFunnelResults().add(match); - } - } - } - - for (final ProcessGroup processGroup : group.getProcessGroups()) { - search(results, search, processGroup); - } - } - - private ComponentSearchResultDTO search(final String searchStr, final Port port) { - final List matches = new ArrayList<>(); - - addIfAppropriate(searchStr, port.getIdentifier(), "Id", matches); - addIfAppropriate(searchStr, port.getVersionedComponentId().orElse(null), "Version Control ID", matches); - addIfAppropriate(searchStr, port.getName(), "Name", matches); - addIfAppropriate(searchStr, port.getComments(), "Comments", matches); - - // consider scheduled state - if (ScheduledState.DISABLED.equals(port.getScheduledState())) { - if (StringUtils.containsIgnoreCase("disabled", searchStr)) { - matches.add("Run status: Disabled"); - } - } else { - if (StringUtils.containsIgnoreCase("invalid", searchStr) && !port.isValid()) { - matches.add("Run status: Invalid"); - } else if (ScheduledState.RUNNING.equals(port.getScheduledState()) && StringUtils.containsIgnoreCase("running", searchStr)) { - matches.add("Run status: Running"); - } else if (ScheduledState.STOPPED.equals(port.getScheduledState()) && StringUtils.containsIgnoreCase("stopped", searchStr)) { - matches.add("Run status: Stopped"); - } - } - - if (port instanceof RootGroupPort) { - final RootGroupPort rootGroupPort = (RootGroupPort) port; - - // user access controls - for (final String userAccessControl : rootGroupPort.getUserAccessControl()) { - addIfAppropriate(searchStr, userAccessControl, "User access control", matches); - } - - // group access controls - for (final String groupAccessControl : rootGroupPort.getGroupAccessControl()) { - addIfAppropriate(searchStr, groupAccessControl, "Group access control", matches); - } - } - - if (matches.isEmpty()) { - return null; - } - - final ComponentSearchResultDTO dto = new ComponentSearchResultDTO(); - dto.setId(port.getIdentifier()); - dto.setName(port.getName()); - dto.setMatches(matches); - return dto; - } - public void verifyComponentTypes(VersionedProcessGroup versionedFlow) { flowController.verifyComponentTypesInSnippet(versionedFlow); } - - private ComponentSearchResultDTO search(final String searchStr, final ProcessorNode procNode) { - final List matches = new ArrayList<>(); - final Processor processor = procNode.getProcessor(); - - addIfAppropriate(searchStr, procNode.getIdentifier(), "Id", matches); - addIfAppropriate(searchStr, procNode.getVersionedComponentId().orElse(null), "Version Control ID", matches); - addIfAppropriate(searchStr, procNode.getName(), "Name", matches); - addIfAppropriate(searchStr, procNode.getComments(), "Comments", matches); - - // consider scheduling strategy - if (SchedulingStrategy.EVENT_DRIVEN.equals(procNode.getSchedulingStrategy()) && StringUtils.containsIgnoreCase("event", searchStr)) { - matches.add("Scheduling strategy: Event driven"); - } else if (SchedulingStrategy.TIMER_DRIVEN.equals(procNode.getSchedulingStrategy()) && StringUtils.containsIgnoreCase("timer", searchStr)) { - matches.add("Scheduling strategy: Timer driven"); - } else if (SchedulingStrategy.PRIMARY_NODE_ONLY.equals(procNode.getSchedulingStrategy()) && StringUtils.containsIgnoreCase("primary", searchStr)) { - // PRIMARY_NODE_ONLY has been deprecated as a SchedulingStrategy and replaced by PRIMARY as an ExecutionNode. - matches.add("Scheduling strategy: On primary node"); - } - - // consider execution node - if (ExecutionNode.PRIMARY.equals(procNode.getExecutionNode()) && StringUtils.containsIgnoreCase("primary", searchStr)) { - matches.add("Execution node: primary"); - } - - // consider scheduled state - if (ScheduledState.DISABLED.equals(procNode.getScheduledState())) { - if (StringUtils.containsIgnoreCase("disabled", searchStr)) { - matches.add("Run status: Disabled"); - } - } else { - if (StringUtils.containsIgnoreCase("invalid", searchStr) && !procNode.isValid()) { - matches.add("Run status: Invalid"); - } else if (ScheduledState.RUNNING.equals(procNode.getScheduledState()) && StringUtils.containsIgnoreCase("running", searchStr)) { - matches.add("Run status: Running"); - } else if (ScheduledState.STOPPED.equals(procNode.getScheduledState()) && StringUtils.containsIgnoreCase("stopped", searchStr)) { - matches.add("Run status: Stopped"); - } - } - - for (final Relationship relationship : procNode.getRelationships()) { - addIfAppropriate(searchStr, relationship.getName(), "Relationship", matches); - } - - // Add both the actual class name and the component type. This allows us to search for 'Ghost' - // to search for components that could not be instantiated. - addIfAppropriate(searchStr, processor.getClass().getSimpleName(), "Type", matches); - addIfAppropriate(searchStr, procNode.getComponentType(), "Type", matches); - - for (final Map.Entry entry : procNode.getProperties().entrySet()) { - final PropertyDescriptor descriptor = entry.getKey(); - - addIfAppropriate(searchStr, descriptor.getName(), "Property name", matches); - addIfAppropriate(searchStr, descriptor.getDescription(), "Property description", matches); - - // never include sensitive properties values in search results - if (descriptor.isSensitive()) { - continue; - } - - String value = entry.getValue(); - - // if unset consider default value - if (value == null) { - value = descriptor.getDefaultValue(); - } - - // evaluate if the value matches the search criteria - if (StringUtils.containsIgnoreCase(value, searchStr)) { - matches.add("Property value: " + descriptor.getName() + " - " + value); - } - } - - // consider searching the processor directly - if (processor instanceof Searchable) { - final Searchable searchable = (Searchable) processor; - - final SearchContext context = new StandardSearchContext(searchStr, procNode, flowController, variableRegistry); - - // search the processor using the appropriate thread context classloader - try (final NarCloseable x = NarCloseable.withComponentNarLoader(processor.getClass(), processor.getIdentifier())) { - final Collection searchResults = searchable.search(context); - if (CollectionUtils.isNotEmpty(searchResults)) { - for (final SearchResult searchResult : searchResults) { - matches.add(searchResult.getLabel() + ": " + searchResult.getMatch()); - } - } - } catch (final Throwable t) { - // log this as error - } - } - - if (matches.isEmpty()) { - return null; - } - - final ComponentSearchResultDTO result = new ComponentSearchResultDTO(); - result.setId(procNode.getIdentifier()); - result.setMatches(matches); - result.setName(procNode.getName()); - return result; - } - - private ComponentSearchResultDTO search(final String searchStr, final ProcessGroup group) { - final List matches = new ArrayList<>(); - final ProcessGroup parent = group.getParent(); - if (parent == null) { - return null; - } - - addIfAppropriate(searchStr, group.getIdentifier(), "Id", matches); - addIfAppropriate(searchStr, group.getVersionedComponentId().orElse(null), "Version Control ID", matches); - addIfAppropriate(searchStr, group.getName(), "Name", matches); - addIfAppropriate(searchStr, group.getComments(), "Comments", matches); - - final ComponentVariableRegistry varRegistry = group.getVariableRegistry(); - if (varRegistry != null) { - final Map variableMap = varRegistry.getVariableMap(); - for (final Map.Entry entry : variableMap.entrySet()) { - addIfAppropriate(searchStr, entry.getKey().getName(), "Variable Name", matches); - addIfAppropriate(searchStr, entry.getValue(), "Variable Value", matches); - } - } - - - if (matches.isEmpty()) { - return null; - } - - final ComponentSearchResultDTO result = new ComponentSearchResultDTO(); - result.setId(group.getIdentifier()); - result.setName(group.getName()); - result.setGroupId(parent.getIdentifier()); - result.setMatches(matches); - return result; - } - - private ComponentSearchResultDTO search(final String searchStr, final Connection connection) { - final List matches = new ArrayList<>(); - - // search id and name - addIfAppropriate(searchStr, connection.getIdentifier(), "Id", matches); - addIfAppropriate(searchStr, connection.getVersionedComponentId().orElse(null), "Version Control ID", matches); - addIfAppropriate(searchStr, connection.getName(), "Name", matches); - - // search relationships - for (final Relationship relationship : connection.getRelationships()) { - addIfAppropriate(searchStr, relationship.getName(), "Relationship", matches); - } - - // search prioritizers - final FlowFileQueue queue = connection.getFlowFileQueue(); - for (final FlowFilePrioritizer comparator : queue.getPriorities()) { - addIfAppropriate(searchStr, comparator.getClass().getName(), "Prioritizer", matches); - } - - // search expiration - if (StringUtils.containsIgnoreCase("expires", searchStr) || StringUtils.containsIgnoreCase("expiration", searchStr)) { - final int expirationMillis = connection.getFlowFileQueue().getFlowFileExpiration(TimeUnit.MILLISECONDS); - if (expirationMillis > 0) { - matches.add("FlowFile expiration: " + connection.getFlowFileQueue().getFlowFileExpiration()); - } - } - - // search back pressure - if (StringUtils.containsIgnoreCase("back pressure", searchStr) || StringUtils.containsIgnoreCase("pressure", searchStr)) { - final String backPressureDataSize = connection.getFlowFileQueue().getBackPressureDataSizeThreshold(); - final Double backPressureBytes = DataUnit.parseDataSize(backPressureDataSize, DataUnit.B); - if (backPressureBytes > 0) { - matches.add("Back pressure data size: " + backPressureDataSize); - } - - final long backPressureCount = connection.getFlowFileQueue().getBackPressureObjectThreshold(); - if (backPressureCount > 0) { - matches.add("Back pressure count: " + backPressureCount); - } - } - - // search the source - final Connectable source = connection.getSource(); - addIfAppropriate(searchStr, source.getIdentifier(), "Source id", matches); - addIfAppropriate(searchStr, source.getName(), "Source name", matches); - addIfAppropriate(searchStr, source.getComments(), "Source comments", matches); - - // search the destination - final Connectable destination = connection.getDestination(); - addIfAppropriate(searchStr, destination.getIdentifier(), "Destination id", matches); - addIfAppropriate(searchStr, destination.getName(), "Destination name", matches); - addIfAppropriate(searchStr, destination.getComments(), "Destination comments", matches); - - if (matches.isEmpty()) { - return null; - } - - final ComponentSearchResultDTO result = new ComponentSearchResultDTO(); - result.setId(connection.getIdentifier()); - - // determine the name of the search match - if (StringUtils.isNotBlank(connection.getName())) { - result.setName(connection.getName()); - } else if (!connection.getRelationships().isEmpty()) { - final List relationships = new ArrayList<>(connection.getRelationships().size()); - for (final Relationship relationship : connection.getRelationships()) { - if (StringUtils.isNotBlank(relationship.getName())) { - relationships.add(relationship.getName()); - } - } - if (!relationships.isEmpty()) { - result.setName(StringUtils.join(relationships, ", ")); - } - } - - // ensure a name is added - if (result.getName() == null) { - result.setName("From source " + connection.getSource().getName()); - } - - result.setMatches(matches); - return result; - } - - private ComponentSearchResultDTO search(final String searchStr, final RemoteProcessGroup group) { - final List matches = new ArrayList<>(); - addIfAppropriate(searchStr, group.getIdentifier(), "Id", matches); - addIfAppropriate(searchStr, group.getVersionedComponentId().orElse(null), "Version Control ID", matches); - addIfAppropriate(searchStr, group.getName(), "Name", matches); - addIfAppropriate(searchStr, group.getComments(), "Comments", matches); - addIfAppropriate(searchStr, group.getTargetUris(), "URLs", matches); - - // consider the transmission status - if ((StringUtils.containsIgnoreCase("transmitting", searchStr) || StringUtils.containsIgnoreCase("transmission enabled", searchStr)) && group.isTransmitting()) { - matches.add("Transmission: On"); - } else if ((StringUtils.containsIgnoreCase("not transmitting", searchStr) || StringUtils.containsIgnoreCase("transmission disabled", searchStr)) && !group.isTransmitting()) { - matches.add("Transmission: Off"); - } - - if (matches.isEmpty()) { - return null; - } - - final ComponentSearchResultDTO result = new ComponentSearchResultDTO(); - result.setId(group.getIdentifier()); - result.setName(group.getName()); - result.setMatches(matches); - return result; - } - - private ComponentSearchResultDTO search(final String searchStr, final Funnel funnel) { - final List matches = new ArrayList<>(); - addIfAppropriate(searchStr, funnel.getIdentifier(), "Id", matches); - addIfAppropriate(searchStr, funnel.getVersionedComponentId().orElse(null), "Version Control ID", matches); - - if (matches.isEmpty()) { - return null; - } - - final ComponentSearchResultDTO dto = new ComponentSearchResultDTO(); - dto.setId(funnel.getIdentifier()); - dto.setName(funnel.getName()); - dto.setMatches(matches); - return dto; - } - - private void addIfAppropriate(final String searchStr, final String value, final String label, final List matches) { - if (StringUtils.containsIgnoreCase(value, searchStr)) { - matches.add(label + ": " + value); - } - } - /* * setters */ @@ -1953,4 +1548,8 @@ public class ControllerFacade implements Authorizable { public void setVariableRegistry(VariableRegistry variableRegistry) { this.variableRegistry = variableRegistry; } + + public void setControllerSearchService(ControllerSearchService controllerSearchService) { + this.controllerSearchService = controllerSearchService; + } } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerSearchService.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerSearchService.java new file mode 100644 index 0000000000..0194b8c18b --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerSearchService.java @@ -0,0 +1,543 @@ +/* + * 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.web.controller; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.authorization.Authorizer; +import org.apache.nifi.authorization.RequestAction; +import org.apache.nifi.authorization.user.NiFiUser; +import org.apache.nifi.authorization.user.NiFiUserUtils; +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.connectable.Connectable; +import org.apache.nifi.connectable.Connection; +import org.apache.nifi.connectable.Funnel; +import org.apache.nifi.connectable.Port; +import org.apache.nifi.controller.FlowController; +import org.apache.nifi.controller.ProcessorNode; +import org.apache.nifi.controller.ScheduledState; +import org.apache.nifi.controller.queue.FlowFileQueue; +import org.apache.nifi.flowfile.FlowFilePrioritizer; +import org.apache.nifi.groups.ProcessGroup; +import org.apache.nifi.groups.RemoteProcessGroup; +import org.apache.nifi.nar.NarCloseable; +import org.apache.nifi.processor.DataUnit; +import org.apache.nifi.processor.Processor; +import org.apache.nifi.processor.Relationship; +import org.apache.nifi.registry.ComponentVariableRegistry; +import org.apache.nifi.registry.VariableDescriptor; +import org.apache.nifi.registry.VariableRegistry; +import org.apache.nifi.remote.RootGroupPort; +import org.apache.nifi.scheduling.ExecutionNode; +import org.apache.nifi.scheduling.SchedulingStrategy; +import org.apache.nifi.search.SearchContext; +import org.apache.nifi.search.SearchResult; +import org.apache.nifi.search.Searchable; +import org.apache.nifi.web.api.dto.search.ComponentSearchResultDTO; +import org.apache.nifi.web.api.dto.search.SearchResultGroupDTO; +import org.apache.nifi.web.api.dto.search.SearchResultsDTO; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * NiFi web controller's helper service that implements component search. + */ +public class ControllerSearchService { + private FlowController flowController; + private Authorizer authorizer; + private VariableRegistry variableRegistry; + + /** + * Searches term in the controller beginning from a given process group. + * + * @param results Search results + * @param search The search term + * @param group The init process group + */ + public void search(final SearchResultsDTO results, final String search, final ProcessGroup group) { + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + + if (group.isAuthorized(authorizer, RequestAction.READ, user)) { + final ComponentSearchResultDTO groupMatch = search(search, group); + if (groupMatch != null) { + // get the parent group, not the current one + groupMatch.setParentGroup(buildResultGroup(group.getParent(), user)); + groupMatch.setVersionedGroup(buildVersionedGroup(group.getParent(), user)); + results.getProcessGroupResults().add(groupMatch); + } + } + + for (final ProcessorNode procNode : group.getProcessors()) { + if (procNode.isAuthorized(authorizer, RequestAction.READ, user)) { + final ComponentSearchResultDTO match = search(search, procNode); + if (match != null) { + match.setGroupId(group.getIdentifier()); + match.setParentGroup(buildResultGroup(group, user)); + match.setVersionedGroup(buildVersionedGroup(group, user)); + results.getProcessorResults().add(match); + } + } + } + + for (final Connection connection : group.getConnections()) { + if (connection.isAuthorized(authorizer, RequestAction.READ, user)) { + final ComponentSearchResultDTO match = search(search, connection); + if (match != null) { + match.setGroupId(group.getIdentifier()); + match.setParentGroup(buildResultGroup(group, user)); + match.setVersionedGroup(buildVersionedGroup(group, user)); + results.getConnectionResults().add(match); + } + } + } + + for (final RemoteProcessGroup remoteGroup : group.getRemoteProcessGroups()) { + if (remoteGroup.isAuthorized(authorizer, RequestAction.READ, user)) { + final ComponentSearchResultDTO match = search(search, remoteGroup); + if (match != null) { + match.setGroupId(group.getIdentifier()); + match.setParentGroup(buildResultGroup(group, user)); + match.setVersionedGroup(buildVersionedGroup(group, user)); + results.getRemoteProcessGroupResults().add(match); + } + } + } + + for (final Port port : group.getInputPorts()) { + if (port.isAuthorized(authorizer, RequestAction.READ, user)) { + final ComponentSearchResultDTO match = search(search, port); + if (match != null) { + match.setGroupId(group.getIdentifier()); + match.setParentGroup(buildResultGroup(group, user)); + match.setVersionedGroup(buildVersionedGroup(group, user)); + results.getInputPortResults().add(match); + } + } + } + + for (final Port port : group.getOutputPorts()) { + if (port.isAuthorized(authorizer, RequestAction.READ, user)) { + final ComponentSearchResultDTO match = search(search, port); + if (match != null) { + match.setGroupId(group.getIdentifier()); + match.setParentGroup(buildResultGroup(group, user)); + match.setVersionedGroup(buildVersionedGroup(group, user)); + results.getOutputPortResults().add(match); + } + } + } + + for (final Funnel funnel : group.getFunnels()) { + if (funnel.isAuthorized(authorizer, RequestAction.READ, user)) { + final ComponentSearchResultDTO match = search(search, funnel); + if (match != null) { + match.setGroupId(group.getIdentifier()); + match.setParentGroup(buildResultGroup(group, user)); + match.setVersionedGroup(buildVersionedGroup(group, user)); + results.getFunnelResults().add(match); + } + } + } + + for (final ProcessGroup processGroup : group.getProcessGroups()) { + search(results, search, processGroup); + } + } + + private ComponentSearchResultDTO search(final String searchStr, final Port port) { + final List matches = new ArrayList<>(); + + addIfAppropriate(searchStr, port.getIdentifier(), "Id", matches); + addIfAppropriate(searchStr, port.getVersionedComponentId().orElse(null), "Version Control ID", matches); + addIfAppropriate(searchStr, port.getName(), "Name", matches); + addIfAppropriate(searchStr, port.getComments(), "Comments", matches); + + // consider scheduled state + if (ScheduledState.DISABLED.equals(port.getScheduledState())) { + if (StringUtils.containsIgnoreCase("disabled", searchStr)) { + matches.add("Run status: Disabled"); + } + } else { + if (StringUtils.containsIgnoreCase("invalid", searchStr) && !port.isValid()) { + matches.add("Run status: Invalid"); + } else if (ScheduledState.RUNNING.equals(port.getScheduledState()) && StringUtils.containsIgnoreCase("running", searchStr)) { + matches.add("Run status: Running"); + } else if (ScheduledState.STOPPED.equals(port.getScheduledState()) && StringUtils.containsIgnoreCase("stopped", searchStr)) { + matches.add("Run status: Stopped"); + } + } + + if (port instanceof RootGroupPort) { + final RootGroupPort rootGroupPort = (RootGroupPort) port; + + // user access controls + for (final String userAccessControl : rootGroupPort.getUserAccessControl()) { + addIfAppropriate(searchStr, userAccessControl, "User access control", matches); + } + + // group access controls + for (final String groupAccessControl : rootGroupPort.getGroupAccessControl()) { + addIfAppropriate(searchStr, groupAccessControl, "Group access control", matches); + } + } + + if (matches.isEmpty()) { + return null; + } + + final ComponentSearchResultDTO dto = new ComponentSearchResultDTO(); + dto.setId(port.getIdentifier()); + dto.setName(port.getName()); + dto.setMatches(matches); + return dto; + } + + private ComponentSearchResultDTO search(final String searchStr, final ProcessorNode procNode) { + final List matches = new ArrayList<>(); + final Processor processor = procNode.getProcessor(); + + addIfAppropriate(searchStr, procNode.getIdentifier(), "Id", matches); + addIfAppropriate(searchStr, procNode.getVersionedComponentId().orElse(null), "Version Control ID", matches); + addIfAppropriate(searchStr, procNode.getName(), "Name", matches); + addIfAppropriate(searchStr, procNode.getComments(), "Comments", matches); + + // consider scheduling strategy + if (SchedulingStrategy.EVENT_DRIVEN.equals(procNode.getSchedulingStrategy()) && StringUtils.containsIgnoreCase("event", searchStr)) { + matches.add("Scheduling strategy: Event driven"); + } else if (SchedulingStrategy.TIMER_DRIVEN.equals(procNode.getSchedulingStrategy()) && StringUtils.containsIgnoreCase("timer", searchStr)) { + matches.add("Scheduling strategy: Timer driven"); + } else if (SchedulingStrategy.PRIMARY_NODE_ONLY.equals(procNode.getSchedulingStrategy()) && StringUtils.containsIgnoreCase("primary", searchStr)) { + // PRIMARY_NODE_ONLY has been deprecated as a SchedulingStrategy and replaced by PRIMARY as an ExecutionNode. + matches.add("Scheduling strategy: On primary node"); + } + + // consider execution node + if (ExecutionNode.PRIMARY.equals(procNode.getExecutionNode()) && StringUtils.containsIgnoreCase("primary", searchStr)) { + matches.add("Execution node: primary"); + } + + // consider scheduled state + if (ScheduledState.DISABLED.equals(procNode.getScheduledState())) { + if (StringUtils.containsIgnoreCase("disabled", searchStr)) { + matches.add("Run status: Disabled"); + } + } else { + if (StringUtils.containsIgnoreCase("invalid", searchStr) && !procNode.isValid()) { + matches.add("Run status: Invalid"); + } else if (ScheduledState.RUNNING.equals(procNode.getScheduledState()) && StringUtils.containsIgnoreCase("running", searchStr)) { + matches.add("Run status: Running"); + } else if (ScheduledState.STOPPED.equals(procNode.getScheduledState()) && StringUtils.containsIgnoreCase("stopped", searchStr)) { + matches.add("Run status: Stopped"); + } + } + + for (final Relationship relationship : procNode.getRelationships()) { + addIfAppropriate(searchStr, relationship.getName(), "Relationship", matches); + } + + // Add both the actual class name and the component type. This allows us to search for 'Ghost' + // to search for components that could not be instantiated. + addIfAppropriate(searchStr, processor.getClass().getSimpleName(), "Type", matches); + addIfAppropriate(searchStr, procNode.getComponentType(), "Type", matches); + + for (final Map.Entry entry : procNode.getProperties().entrySet()) { + final PropertyDescriptor descriptor = entry.getKey(); + + addIfAppropriate(searchStr, descriptor.getName(), "Property name", matches); + addIfAppropriate(searchStr, descriptor.getDescription(), "Property description", matches); + + // never include sensitive properties values in search results + if (descriptor.isSensitive()) { + continue; + } + + String value = entry.getValue(); + + // if unset consider default value + if (value == null) { + value = descriptor.getDefaultValue(); + } + + // evaluate if the value matches the search criteria + if (StringUtils.containsIgnoreCase(value, searchStr)) { + matches.add("Property value: " + descriptor.getName() + " - " + value); + } + } + + // consider searching the processor directly + if (processor instanceof Searchable) { + final Searchable searchable = (Searchable) processor; + + final SearchContext context = new StandardSearchContext(searchStr, procNode, flowController, variableRegistry); + + // search the processor using the appropriate thread context classloader + try (final NarCloseable x = NarCloseable.withComponentNarLoader(processor.getClass(), processor.getIdentifier())) { + final Collection searchResults = searchable.search(context); + if (CollectionUtils.isNotEmpty(searchResults)) { + for (final SearchResult searchResult : searchResults) { + matches.add(searchResult.getLabel() + ": " + searchResult.getMatch()); + } + } + } catch (final Throwable t) { + // log this as error + } + } + + if (matches.isEmpty()) { + return null; + } + + final ComponentSearchResultDTO result = new ComponentSearchResultDTO(); + result.setId(procNode.getIdentifier()); + result.setMatches(matches); + result.setName(procNode.getName()); + return result; + } + + private ComponentSearchResultDTO search(final String searchStr, final ProcessGroup group) { + final List matches = new ArrayList<>(); + final ProcessGroup parent = group.getParent(); + if (parent == null) { + return null; + } + + addIfAppropriate(searchStr, group.getIdentifier(), "Id", matches); + addIfAppropriate(searchStr, group.getVersionedComponentId().orElse(null), "Version Control ID", matches); + addIfAppropriate(searchStr, group.getName(), "Name", matches); + addIfAppropriate(searchStr, group.getComments(), "Comments", matches); + + final ComponentVariableRegistry varRegistry = group.getVariableRegistry(); + if (varRegistry != null) { + final Map variableMap = varRegistry.getVariableMap(); + for (final Map.Entry entry : variableMap.entrySet()) { + addIfAppropriate(searchStr, entry.getKey().getName(), "Variable Name", matches); + addIfAppropriate(searchStr, entry.getValue(), "Variable Value", matches); + } + } + + if (matches.isEmpty()) { + return null; + } + + final ComponentSearchResultDTO result = new ComponentSearchResultDTO(); + result.setId(group.getIdentifier()); + result.setName(group.getName()); + result.setGroupId(parent.getIdentifier()); + result.setMatches(matches); + return result; + } + + private ComponentSearchResultDTO search(final String searchStr, final Connection connection) { + final List matches = new ArrayList<>(); + + // search id and name + addIfAppropriate(searchStr, connection.getIdentifier(), "Id", matches); + addIfAppropriate(searchStr, connection.getVersionedComponentId().orElse(null), "Version Control ID", matches); + addIfAppropriate(searchStr, connection.getName(), "Name", matches); + + // search relationships + for (final Relationship relationship : connection.getRelationships()) { + addIfAppropriate(searchStr, relationship.getName(), "Relationship", matches); + } + + // search prioritizers + final FlowFileQueue queue = connection.getFlowFileQueue(); + for (final FlowFilePrioritizer comparator : queue.getPriorities()) { + addIfAppropriate(searchStr, comparator.getClass().getName(), "Prioritizer", matches); + } + + // search expiration + if (StringUtils.containsIgnoreCase("expires", searchStr) || StringUtils.containsIgnoreCase("expiration", searchStr)) { + final int expirationMillis = connection.getFlowFileQueue().getFlowFileExpiration(TimeUnit.MILLISECONDS); + if (expirationMillis > 0) { + matches.add("FlowFile expiration: " + connection.getFlowFileQueue().getFlowFileExpiration()); + } + } + + // search back pressure + if (StringUtils.containsIgnoreCase("back pressure", searchStr) || StringUtils.containsIgnoreCase("pressure", searchStr)) { + final String backPressureDataSize = connection.getFlowFileQueue().getBackPressureDataSizeThreshold(); + final Double backPressureBytes = DataUnit.parseDataSize(backPressureDataSize, DataUnit.B); + if (backPressureBytes > 0) { + matches.add("Back pressure data size: " + backPressureDataSize); + } + + final long backPressureCount = connection.getFlowFileQueue().getBackPressureObjectThreshold(); + if (backPressureCount > 0) { + matches.add("Back pressure count: " + backPressureCount); + } + } + + // search the source + final Connectable source = connection.getSource(); + addIfAppropriate(searchStr, source.getIdentifier(), "Source id", matches); + addIfAppropriate(searchStr, source.getName(), "Source name", matches); + addIfAppropriate(searchStr, source.getComments(), "Source comments", matches); + + // search the destination + final Connectable destination = connection.getDestination(); + addIfAppropriate(searchStr, destination.getIdentifier(), "Destination id", matches); + addIfAppropriate(searchStr, destination.getName(), "Destination name", matches); + addIfAppropriate(searchStr, destination.getComments(), "Destination comments", matches); + + if (matches.isEmpty()) { + return null; + } + + final ComponentSearchResultDTO result = new ComponentSearchResultDTO(); + result.setId(connection.getIdentifier()); + + // determine the name of the search match + if (StringUtils.isNotBlank(connection.getName())) { + result.setName(connection.getName()); + } else if (!connection.getRelationships().isEmpty()) { + final List relationships = new ArrayList<>(connection.getRelationships().size()); + for (final Relationship relationship : connection.getRelationships()) { + if (StringUtils.isNotBlank(relationship.getName())) { + relationships.add(relationship.getName()); + } + } + if (!relationships.isEmpty()) { + result.setName(StringUtils.join(relationships, ", ")); + } + } + + // ensure a name is added + if (result.getName() == null) { + result.setName("From source " + connection.getSource().getName()); + } + + result.setMatches(matches); + return result; + } + + private ComponentSearchResultDTO search(final String searchStr, final RemoteProcessGroup group) { + final List matches = new ArrayList<>(); + addIfAppropriate(searchStr, group.getIdentifier(), "Id", matches); + addIfAppropriate(searchStr, group.getVersionedComponentId().orElse(null), "Version Control ID", matches); + addIfAppropriate(searchStr, group.getName(), "Name", matches); + addIfAppropriate(searchStr, group.getComments(), "Comments", matches); + addIfAppropriate(searchStr, group.getTargetUris(), "URLs", matches); + + // consider the transmission status + if ((StringUtils.containsIgnoreCase("transmitting", searchStr) || StringUtils.containsIgnoreCase("transmission enabled", searchStr)) && group.isTransmitting()) { + matches.add("Transmission: On"); + } else if ((StringUtils.containsIgnoreCase("not transmitting", searchStr) || StringUtils.containsIgnoreCase("transmission disabled", searchStr)) && !group.isTransmitting()) { + matches.add("Transmission: Off"); + } + + if (matches.isEmpty()) { + return null; + } + + final ComponentSearchResultDTO result = new ComponentSearchResultDTO(); + result.setId(group.getIdentifier()); + result.setName(group.getName()); + result.setMatches(matches); + return result; + } + + private ComponentSearchResultDTO search(final String searchStr, final Funnel funnel) { + final List matches = new ArrayList<>(); + addIfAppropriate(searchStr, funnel.getIdentifier(), "Id", matches); + addIfAppropriate(searchStr, funnel.getVersionedComponentId().orElse(null), "Version Control ID", matches); + + if (matches.isEmpty()) { + return null; + } + + final ComponentSearchResultDTO dto = new ComponentSearchResultDTO(); + dto.setId(funnel.getIdentifier()); + dto.setName(funnel.getName()); + dto.setMatches(matches); + return dto; + } + + /** + * Builds the nearest versioned parent result group for a given user. + * + * @param group The containing group + * @param user The current NiFi user + * @return Versioned parent group + */ + private SearchResultGroupDTO buildVersionedGroup(final ProcessGroup group, final NiFiUser user) { + if (group == null) { + return null; + } + + ProcessGroup tmpParent = group.getParent(); + ProcessGroup tmpGroup = group; + + // search for a versioned group by traversing the group tree up to the root + while (!tmpGroup.isRootGroup()) { + if (tmpGroup.getVersionControlInformation() != null) { + return buildResultGroup(tmpGroup, user); + } + + tmpGroup = tmpParent; + tmpParent = tmpGroup.getParent(); + } + + // traversed all the way to the root + return null; + } + + /** + * Builds result group for a given user. + * + * @param group The containing group + * @param user The current NiFi user + * @return Result group + */ + private SearchResultGroupDTO buildResultGroup(final ProcessGroup group, final NiFiUser user) { + if (group == null) { + return null; + } + + final SearchResultGroupDTO resultGroup = new SearchResultGroupDTO(); + resultGroup.setId(group.getIdentifier()); + + // keep the group name confidential + if (group.isAuthorized(authorizer, RequestAction.READ, user)) { + resultGroup.setName(group.getName()); + } + + return resultGroup; + } + + private void addIfAppropriate(final String searchStr, final String value, final String label, final List matches) { + if (StringUtils.containsIgnoreCase(value, searchStr)) { + matches.add(label + ": " + value); + } + } + + public void setFlowController(FlowController flowController) { + this.flowController = flowController; + } + + public void setAuthorizer(Authorizer authorizer) { + this.authorizer = authorizer; + } + + public void setVariableRegistry(VariableRegistry variableRegistry) { + this.variableRegistry = variableRegistry; + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml index ebab24e71f..8c1e2ad03c 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml @@ -129,6 +129,11 @@ + + + + + @@ -136,6 +141,7 @@ + diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/controller/ControllerSearchServiceTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/controller/ControllerSearchServiceTest.java new file mode 100644 index 0000000000..5b8666f85e --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/controller/ControllerSearchServiceTest.java @@ -0,0 +1,405 @@ +/* + * 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.web.controller; + +import org.apache.nifi.authorization.Authorizer; +import org.apache.nifi.authorization.RequestAction; +import org.apache.nifi.authorization.user.NiFiUser; +import org.apache.nifi.controller.ProcessorNode; +import org.apache.nifi.controller.StandardProcessorNode; +import org.apache.nifi.groups.ProcessGroup; +import org.apache.nifi.processor.Processor; +import org.apache.nifi.registry.VariableRegistry; +import org.apache.nifi.registry.flow.StandardVersionControlInformation; +import org.apache.nifi.registry.flow.VersionControlInformation; +import org.apache.nifi.registry.variable.MutableVariableRegistry; +import org.apache.nifi.web.api.dto.search.SearchResultsDTO; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import java.util.HashSet; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; + +public class ControllerSearchServiceTest { + private MutableVariableRegistry variableRegistry; + private ControllerSearchService service; + private SearchResultsDTO searchResultsDTO; + + @Before + public void setUp() { + variableRegistry = mock(MutableVariableRegistry.class); + service = new ControllerSearchService(); + searchResultsDTO = new SearchResultsDTO(); + } + + @Test + public void testSearchInRootLevelAllAuthorizedNoVersionControl() { + // root level PG + final ProcessGroup rootProcessGroup = setupMockedProcessGroup("root", null, true, variableRegistry, null); + + // first level PGs + final ProcessGroup firstLevelAProcessGroup = setupMockedProcessGroup("firstLevelA", rootProcessGroup, true, variableRegistry, null); + final ProcessGroup firstLevelBProcessGroup = setupMockedProcessGroup("firstLevelB", rootProcessGroup, true, variableRegistry, null); + + // second level PGs + final ProcessGroup secondLevelAProcessGroup = setupMockedProcessGroup("secondLevelA", firstLevelAProcessGroup, true, variableRegistry, null); + final ProcessGroup secondLevelBProcessGroup = setupMockedProcessGroup("secondLevelB", firstLevelBProcessGroup, true, variableRegistry, null); + // third level PGs + final ProcessGroup thirdLevelAProcessGroup = setupMockedProcessGroup("thirdLevelA", secondLevelAProcessGroup, true, variableRegistry, null); + final ProcessGroup thirdLevelBProcessGroup = setupMockedProcessGroup("thirdLevelB", secondLevelAProcessGroup, true, variableRegistry, null); + + // link PGs together + Mockito.doReturn(new HashSet() { + { + add(firstLevelAProcessGroup); + add(firstLevelBProcessGroup); + } + }).when(rootProcessGroup).getProcessGroups(); + + Mockito.doReturn(new HashSet() { + { + add(secondLevelAProcessGroup); + } + }).when(firstLevelAProcessGroup).getProcessGroups(); + Mockito.doReturn(new HashSet() { + { + add(secondLevelBProcessGroup); + } + }).when(firstLevelBProcessGroup).getProcessGroups(); + + Mockito.doReturn(new HashSet() { + { + add(thirdLevelAProcessGroup); + add(thirdLevelBProcessGroup); + } + }).when(secondLevelAProcessGroup).getProcessGroups(); + + // setup processor + setupMockedProcessor("foobar", rootProcessGroup, true, variableRegistry); + + // perform search + service.search(searchResultsDTO, "foo", rootProcessGroup); + + assertTrue(searchResultsDTO.getProcessorResults().size() == 1); + assertTrue(searchResultsDTO.getProcessorResults().get(0).getId().equals("foobarId")); + assertTrue(searchResultsDTO.getProcessorResults().get(0).getParentGroup().getId().equals("rootId")); + assertTrue(searchResultsDTO.getProcessorResults().get(0).getParentGroup().getName().equals("root")); + assertTrue(searchResultsDTO.getProcessorResults().get(0).getVersionedGroup() == null); + } + + @Test + public void testSearchInThirdLevelAllAuthorizedNoVersionControl() { + // root level PG + final ProcessGroup rootProcessGroup = setupMockedProcessGroup("root", null, true, variableRegistry, null); + + // first level PGs + final ProcessGroup firstLevelAProcessGroup = setupMockedProcessGroup("firstLevelA", rootProcessGroup, true, variableRegistry, null); + final ProcessGroup firstLevelBProcessGroup = setupMockedProcessGroup("firstLevelB", rootProcessGroup, true, variableRegistry, null); + + // second level PGs + final ProcessGroup secondLevelAProcessGroup = setupMockedProcessGroup("secondLevelA", firstLevelAProcessGroup, true, variableRegistry, null); + final ProcessGroup secondLevelBProcessGroup = setupMockedProcessGroup("secondLevelB", firstLevelBProcessGroup, true, variableRegistry, null); + // third level PGs + final ProcessGroup thirdLevelAProcessGroup = setupMockedProcessGroup("thirdLevelA", secondLevelAProcessGroup, true, variableRegistry, null); + final ProcessGroup thirdLevelBProcessGroup = setupMockedProcessGroup("thirdLevelB", secondLevelAProcessGroup, true, variableRegistry, null); + + // link PGs together + Mockito.doReturn(new HashSet() { + { + add(firstLevelAProcessGroup); + add(firstLevelBProcessGroup); + } + }).when(rootProcessGroup).getProcessGroups(); + + Mockito.doReturn(new HashSet() { + { + add(secondLevelAProcessGroup); + } + }).when(firstLevelAProcessGroup).getProcessGroups(); + + Mockito.doReturn(new HashSet() { + { + add(secondLevelBProcessGroup); + } + }).when(firstLevelBProcessGroup).getProcessGroups(); + + Mockito.doReturn(new HashSet() { + { + add(thirdLevelAProcessGroup); + add(thirdLevelBProcessGroup); + } + }).when(secondLevelAProcessGroup).getProcessGroups(); + + // setup processor + setupMockedProcessor("foobar", thirdLevelAProcessGroup, true, variableRegistry); + + // perform search + service.search(searchResultsDTO, "foo", rootProcessGroup); + + assertTrue(searchResultsDTO.getProcessorResults().size() == 1); + assertTrue(searchResultsDTO.getProcessorResults().get(0).getId().equals("foobarId")); + assertTrue(searchResultsDTO.getProcessorResults().get(0).getParentGroup().getId().equals("thirdLevelAId")); + assertTrue(searchResultsDTO.getProcessorResults().get(0).getParentGroup().getName().equals("thirdLevelA")); + assertTrue(searchResultsDTO.getProcessorResults().get(0).getVersionedGroup() == null); + } + + @Test + public void testSearchInThirdLevelParentNotAuthorizedNoVersionControl() { + // root level PG + final ProcessGroup rootProcessGroup = setupMockedProcessGroup("root", null, true, variableRegistry, null); + + // first level PGs + final ProcessGroup firstLevelAProcessGroup = setupMockedProcessGroup("firstLevelA", rootProcessGroup, true, variableRegistry, null); + final ProcessGroup firstLevelBProcessGroup = setupMockedProcessGroup("firstLevelB", rootProcessGroup, true, variableRegistry, null); + + // second level PGs + final ProcessGroup secondLevelAProcessGroup = setupMockedProcessGroup("secondLevelA", firstLevelAProcessGroup, true, variableRegistry, null); + final ProcessGroup secondLevelBProcessGroup = setupMockedProcessGroup("secondLevelB", firstLevelBProcessGroup, true, variableRegistry, null); + // third level PGs - not authorized + final ProcessGroup thirdLevelAProcessGroup = setupMockedProcessGroup("thirdLevelA", secondLevelAProcessGroup, false, variableRegistry, null); + final ProcessGroup thirdLevelBProcessGroup = setupMockedProcessGroup("thirdLevelB", secondLevelAProcessGroup, false, variableRegistry, null); + + // link PGs together + Mockito.doReturn(new HashSet() { + { + add(firstLevelAProcessGroup); + add(firstLevelBProcessGroup); + } + }).when(rootProcessGroup).getProcessGroups(); + + Mockito.doReturn(new HashSet() { + { + add(secondLevelAProcessGroup); + } + }).when(firstLevelAProcessGroup).getProcessGroups(); + + Mockito.doReturn(new HashSet() { + { + add(secondLevelBProcessGroup); + } + }).when(firstLevelBProcessGroup).getProcessGroups(); + + Mockito.doReturn(new HashSet() { + { + add(thirdLevelAProcessGroup); + add(thirdLevelBProcessGroup); + } + }).when(secondLevelAProcessGroup).getProcessGroups(); + + // setup processor + setupMockedProcessor("foobar", thirdLevelAProcessGroup, true, variableRegistry); + + // perform search + service.search(searchResultsDTO, "foo", rootProcessGroup); + + assertTrue(searchResultsDTO.getProcessorResults().size() == 1); + assertTrue(searchResultsDTO.getProcessorResults().get(0).getId().equals("foobarId")); + assertTrue(searchResultsDTO.getProcessorResults().get(0).getParentGroup().getId().equals("thirdLevelAId")); + assertTrue(searchResultsDTO.getProcessorResults().get(0).getParentGroup().getName() == null); + assertTrue(searchResultsDTO.getProcessorResults().get(0).getVersionedGroup() == null); + } + + @Test + public void testSearchInThirdLevelParentNotAuthorizedWithVersionControl() { + // root level PG + final ProcessGroup rootProcessGroup = setupMockedProcessGroup("root", null, true, variableRegistry, null); + + // first level PGs + final VersionControlInformation versionControlInformation = setupVC(); + final ProcessGroup firstLevelAProcessGroup = setupMockedProcessGroup("firstLevelA", rootProcessGroup, true, variableRegistry, versionControlInformation); + final ProcessGroup firstLevelBProcessGroup = setupMockedProcessGroup("firstLevelB", rootProcessGroup, true, variableRegistry, null); + + // second level PGs + final ProcessGroup secondLevelAProcessGroup = setupMockedProcessGroup("secondLevelA", firstLevelAProcessGroup, true, variableRegistry, null); + final ProcessGroup secondLevelBProcessGroup = setupMockedProcessGroup("secondLevelB", firstLevelBProcessGroup, true, variableRegistry, null); + // third level PGs - not authorized + final ProcessGroup thirdLevelAProcessGroup = setupMockedProcessGroup("thirdLevelA", secondLevelAProcessGroup, false, variableRegistry, null); + final ProcessGroup thirdLevelBProcessGroup = setupMockedProcessGroup("thirdLevelB", secondLevelAProcessGroup, false, variableRegistry, null); + + // link PGs together + Mockito.doReturn(new HashSet() { + { + add(firstLevelAProcessGroup); + add(firstLevelBProcessGroup); + } + }).when(rootProcessGroup).getProcessGroups(); + + Mockito.doReturn(new HashSet() { + { + add(secondLevelAProcessGroup); + } + }).when(firstLevelAProcessGroup).getProcessGroups(); + + Mockito.doReturn(new HashSet() { + { + add(secondLevelBProcessGroup); + } + }).when(firstLevelBProcessGroup).getProcessGroups(); + + Mockito.doReturn(new HashSet() { + { + add(thirdLevelAProcessGroup); + add(thirdLevelBProcessGroup); + } + }).when(secondLevelAProcessGroup).getProcessGroups(); + + // setup processor + setupMockedProcessor("foobar", thirdLevelAProcessGroup, true, variableRegistry); + + // perform search + service.search(searchResultsDTO, "foo", rootProcessGroup); + + assertTrue(searchResultsDTO.getProcessorResults().size() == 1); + assertTrue(searchResultsDTO.getProcessorResults().get(0).getId().equals("foobarId")); + assertTrue(searchResultsDTO.getProcessorResults().get(0).getParentGroup().getId().equals("thirdLevelAId")); + assertTrue(searchResultsDTO.getProcessorResults().get(0).getParentGroup().getName() == null); + assertTrue(searchResultsDTO.getProcessorResults().get(0).getVersionedGroup() != null); + assertTrue(searchResultsDTO.getProcessorResults().get(0).getVersionedGroup().getId().equals("firstLevelAId")); + assertTrue(searchResultsDTO.getProcessorResults().get(0).getVersionedGroup().getName().equals("firstLevelA")); + } + + @Test + public void testSearchInThirdLevelParentNotAuthorizedWithVersionControlInTheGroup() { + // root level PG + final ProcessGroup rootProcessGroup = setupMockedProcessGroup("root", null, true, variableRegistry, null); + + // first level PGs + final ProcessGroup firstLevelAProcessGroup = setupMockedProcessGroup("firstLevelA", rootProcessGroup, true, variableRegistry, null); + final ProcessGroup firstLevelBProcessGroup = setupMockedProcessGroup("firstLevelB", rootProcessGroup, true, variableRegistry, null); + + // second level PGs + final ProcessGroup secondLevelAProcessGroup = setupMockedProcessGroup("secondLevelA", firstLevelAProcessGroup, true, variableRegistry, null); + final ProcessGroup secondLevelBProcessGroup = setupMockedProcessGroup("secondLevelB", firstLevelBProcessGroup, true, variableRegistry, null); + // third level PGs - not authorized + final VersionControlInformation versionControlInformation = setupVC(); + final ProcessGroup thirdLevelAProcessGroup = setupMockedProcessGroup("thirdLevelA", secondLevelAProcessGroup, false, variableRegistry, versionControlInformation); + final ProcessGroup thirdLevelBProcessGroup = setupMockedProcessGroup("thirdLevelB", secondLevelAProcessGroup, false, variableRegistry, null); + + // link PGs together + Mockito.doReturn(new HashSet() { + { + add(firstLevelAProcessGroup); + add(firstLevelBProcessGroup); + } + }).when(rootProcessGroup).getProcessGroups(); + + Mockito.doReturn(new HashSet() { + { + add(secondLevelAProcessGroup); + } + }).when(firstLevelAProcessGroup).getProcessGroups(); + + Mockito.doReturn(new HashSet() { + { + add(secondLevelBProcessGroup); + } + }).when(firstLevelBProcessGroup).getProcessGroups(); + + Mockito.doReturn(new HashSet() { + { + add(thirdLevelAProcessGroup); + add(thirdLevelBProcessGroup); + } + }).when(secondLevelAProcessGroup).getProcessGroups(); + + // setup processor + setupMockedProcessor("foobar", thirdLevelAProcessGroup, true, variableRegistry); + + // perform search + service.search(searchResultsDTO, "foo", rootProcessGroup); + + assertTrue(searchResultsDTO.getProcessorResults().size() == 1); + assertTrue(searchResultsDTO.getProcessorResults().get(0).getId().equals("foobarId")); + assertTrue(searchResultsDTO.getProcessorResults().get(0).getParentGroup().getId().equals("thirdLevelAId")); + assertTrue(searchResultsDTO.getProcessorResults().get(0).getParentGroup().getName() == null); + assertTrue(searchResultsDTO.getProcessorResults().get(0).getVersionedGroup() != null); + assertTrue(searchResultsDTO.getProcessorResults().get(0).getVersionedGroup().getId().equals("thirdLevelAId")); + assertTrue(searchResultsDTO.getProcessorResults().get(0).getVersionedGroup().getName() == null); + } + + /** + * Mocks Processor including isAuthorized() and its name & id. + * + * @param processorName Desired processor name + * @param containingProcessGroup The process group + * @param authorizedToRead Can the processor data be read? + * @param variableRegistry The variable registry + */ + private static void setupMockedProcessor(final String processorName, final ProcessGroup containingProcessGroup, boolean authorizedToRead, final MutableVariableRegistry variableRegistry) { + final String processorId = processorName + "Id"; + final Processor processor1 = mock(Processor.class); + + final ProcessorNode processorNode1 = mock(StandardProcessorNode.class); + Mockito.doReturn(authorizedToRead).when(processorNode1).isAuthorized(any(Authorizer.class), eq(RequestAction.READ), any(NiFiUser.class)); + Mockito.doReturn(variableRegistry).when(processorNode1).getVariableRegistry(); + Mockito.doReturn(processor1).when(processorNode1).getProcessor(); + // set processor node's attributes + Mockito.doReturn(processorId).when(processorNode1).getIdentifier(); + Mockito.doReturn(processorName).when(processorNode1).getName(); + + // assign processor node to its PG + Mockito.doReturn(new HashSet() { + { + add(processorNode1); + } + }).when(containingProcessGroup).getProcessors(); + } + + /** + * Mocks ProcessGroup due to isAuthorized(). The final class StandardProcessGroup can't be used. + * + * @param processGroupName Desired process group name + * @param parent The parent process group + * @param authorizedToRead Can the process group data be read? + * @param variableRegistry The variable registry + * @param versionControlInformation The version control information + * @return Mocked process group + */ + private static ProcessGroup setupMockedProcessGroup(final String processGroupName, final ProcessGroup parent, boolean authorizedToRead, final VariableRegistry variableRegistry, + final VersionControlInformation versionControlInformation) { + final String processGroupId = processGroupName + "Id"; + final ProcessGroup processGroup = mock(ProcessGroup.class); + + Mockito.doReturn(processGroupId).when(processGroup).getIdentifier(); + Mockito.doReturn(processGroupName).when(processGroup).getName(); + Mockito.doReturn(parent).when(processGroup).getParent(); + Mockito.doReturn(versionControlInformation).when(processGroup).getVersionControlInformation(); + Mockito.doReturn(variableRegistry).when(processGroup).getVariableRegistry(); + Mockito.doReturn(parent == null).when(processGroup).isRootGroup(); + // override process group's access rights + Mockito.doReturn(authorizedToRead).when(processGroup).isAuthorized(any(Authorizer.class), eq(RequestAction.READ), any(NiFiUser.class)); + + return processGroup; + } + + /** + * Creates a version control information using dummy attributes. + * + * @return Dummy version control information + */ + private static VersionControlInformation setupVC() { + final StandardVersionControlInformation.Builder builder = new StandardVersionControlInformation.Builder(); + builder.registryId("regId").bucketId("bucId").flowId("flowId").version(1); + + return builder.build(); + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-flow-status-controller.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-flow-status-controller.js index 038a07bf79..67b9d1054d 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-flow-status-controller.js +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-flow-status-controller.js @@ -200,7 +200,23 @@ } }, _renderItem: function (ul, match) { - var itemContent = $('').append($('
').text(match.name)); + var itemHeader = $('
').text(match.name); + + var parentGroupHeader = $('
').append(document.createTextNode('Parent: ')); + var parentGroup = match.parentGroup.name ? match.parentGroup.name : match.parentGroup.id; + parentGroupHeader = parentGroupHeader.append($('').text(parentGroup)); + + var versionedGroupHeader = $('
').append(document.createTextNode('Versioned: ')); + var versionedGroup = '-'; + + if (nfCommon.isDefinedAndNotNull(match.versionedGroup)) { + versionedGroup = match.versionedGroup.name ? match.versionedGroup.name : match.versionedGroup.id; + } + + versionedGroupHeader = versionedGroupHeader.append($('').text(versionedGroup)); + // create a search item wrapper + var itemContent = $('').append(itemHeader).append(parentGroupHeader).append(versionedGroupHeader); + // append all matches $.each(match.matches, function (i, match) { itemContent.append($('
').text(match)); }); @@ -231,9 +247,10 @@ }, select: function (event, ui) { var item = ui.item; + var group = item.parentGroup; // show the selected component - nfCanvasUtils.showComponent(item.groupId, item.id); + nfCanvasUtils.showComponent(group.id, item.id); searchCtrl.getInputElement().val('').blur();