Repository: nifi
Updated Branches:
  refs/heads/master 9f95a10df -> 91e98aa50


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


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/91e98aa5
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/91e98aa5
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/91e98aa5

Branch: refs/heads/master
Commit: 91e98aa50b27800d79c2f9db9a226237b87e65cb
Parents: 9f95a10
Author: yuri1969 <1969yuri1...@gmail.com>
Authored: Fri Dec 22 20:18:20 2017 +0100
Committer: Matt Gilman <matt.c.gil...@gmail.com>
Committed: Tue Feb 20 12:42:09 2018 -0500

----------------------------------------------------------------------
 .../dto/search/ComponentSearchResultDTO.java    |  30 +
 .../api/dto/search/SearchResultGroupDTO.java    |  59 ++
 .../nifi/web/controller/ControllerFacade.java   | 415 +-------------
 .../web/controller/ControllerSearchService.java | 543 +++++++++++++++++++
 .../src/main/resources/nifi-web-api-context.xml |   6 +
 .../controller/ControllerSearchServiceTest.java | 405 ++++++++++++++
 .../nf-ng-canvas-flow-status-controller.js      |  21 +-
 7 files changed, 1069 insertions(+), 410 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/91e98aa5/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/search/ComponentSearchResultDTO.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 7cddd7e..ab46ad8 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<String> matches;
 
@@ -61,6 +63,34 @@ public class ComponentSearchResultDTO {
     }
 
     /**
+     * @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
      */
     @ApiModelProperty(

http://git-wip-us.apache.org/repos/asf/nifi/blob/91e98aa5/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/search/SearchResultGroupDTO.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/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 0000000..534cc8b
--- /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;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/91e98aa5/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java
----------------------------------------------------------------------
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 6cef841..72702a6 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);
-
-        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<String> 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);
-            }
-        }
+        controllerSearchService.search(results, search, rootGroup);
 
-        if (matches.isEmpty()) {
-            return null;
-        }
-
-        final ComponentSearchResultDTO dto = new ComponentSearchResultDTO();
-        dto.setId(port.getIdentifier());
-        dto.setName(port.getName());
-        dto.setMatches(matches);
-        return dto;
+        return results;
     }
 
     public void verifyComponentTypes(VersionedProcessGroup versionedFlow) {
         flowController.verifyComponentTypesInSnippet(versionedFlow);
     }
 
-
-    private ComponentSearchResultDTO search(final String searchStr, final 
ProcessorNode procNode) {
-        final List<String> 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<PropertyDescriptor, String> 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<SearchResult> 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<String> 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<VariableDescriptor, String> variableMap = 
varRegistry.getVariableMap();
-            for (final Map.Entry<VariableDescriptor, String> 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<String> 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<String> 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<String> 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<String> 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<String> 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;
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/91e98aa5/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerSearchService.java
----------------------------------------------------------------------
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 0000000..0194b8c
--- /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<String> 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<String> 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<PropertyDescriptor, String> 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<SearchResult> 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<String> 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<VariableDescriptor, String> variableMap = 
varRegistry.getVariableMap();
+            for (final Map.Entry<VariableDescriptor, String> 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<String> 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<String> 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<String> 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<String> 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<String> 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;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/91e98aa5/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml
----------------------------------------------------------------------
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 ebab24e..8c1e2ad 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 @@
     <bean id="policyBasedAuthorizerDAO" 
class="org.apache.nifi.web.dao.impl.StandardPolicyBasedAuthorizerDAO">
         <constructor-arg ref="authorizer"/>
     </bean>
+    <bean id="controllerSearchService" 
class="org.apache.nifi.web.controller.ControllerSearchService">
+        <property name="flowController" ref="flowController"/>
+        <property name="authorizer" ref="authorizer"/>
+        <property name="variableRegistry" ref="variableRegistry"/>
+    </bean>
     <bean id="controllerFacade" 
class="org.apache.nifi.web.controller.ControllerFacade">
         <property name="properties" ref="nifiProperties"/>
         <property name="flowController" ref="flowController"/>
@@ -136,6 +141,7 @@
         <property name="authorizer" ref="authorizer"/>
         <property name="dtoFactory" ref="dtoFactory"/>
         <property name="variableRegistry" ref="variableRegistry"/>
+        <property name="controllerSearchService" 
ref="controllerSearchService"/>
     </bean>
     <bean id="authorizableLookup" 
class="org.apache.nifi.authorization.StandardAuthorizableLookup">
         <property name="controllerFacade" ref="controllerFacade"/>

http://git-wip-us.apache.org/repos/asf/nifi/blob/91e98aa5/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-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 0000000..5b8666f
--- /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<ProcessGroup>() {
+            {
+                add(firstLevelAProcessGroup);
+                add(firstLevelBProcessGroup);
+            }
+        }).when(rootProcessGroup).getProcessGroups();
+
+        Mockito.doReturn(new HashSet<ProcessGroup>() {
+            {
+                add(secondLevelAProcessGroup);
+            }
+        }).when(firstLevelAProcessGroup).getProcessGroups();
+        Mockito.doReturn(new HashSet<ProcessGroup>() {
+            {
+                add(secondLevelBProcessGroup);
+            }
+        }).when(firstLevelBProcessGroup).getProcessGroups();
+
+        Mockito.doReturn(new HashSet<ProcessGroup>() {
+            {
+                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<ProcessGroup>() {
+            {
+                add(firstLevelAProcessGroup);
+                add(firstLevelBProcessGroup);
+            }
+        }).when(rootProcessGroup).getProcessGroups();
+
+        Mockito.doReturn(new HashSet<ProcessGroup>() {
+            {
+                add(secondLevelAProcessGroup);
+            }
+        }).when(firstLevelAProcessGroup).getProcessGroups();
+
+        Mockito.doReturn(new HashSet<ProcessGroup>() {
+            {
+                add(secondLevelBProcessGroup);
+            }
+        }).when(firstLevelBProcessGroup).getProcessGroups();
+
+        Mockito.doReturn(new HashSet<ProcessGroup>() {
+            {
+                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<ProcessGroup>() {
+            {
+                add(firstLevelAProcessGroup);
+                add(firstLevelBProcessGroup);
+            }
+        }).when(rootProcessGroup).getProcessGroups();
+
+        Mockito.doReturn(new HashSet<ProcessGroup>() {
+            {
+                add(secondLevelAProcessGroup);
+            }
+        }).when(firstLevelAProcessGroup).getProcessGroups();
+
+        Mockito.doReturn(new HashSet<ProcessGroup>() {
+            {
+                add(secondLevelBProcessGroup);
+            }
+        }).when(firstLevelBProcessGroup).getProcessGroups();
+
+        Mockito.doReturn(new HashSet<ProcessGroup>() {
+            {
+                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<ProcessGroup>() {
+            {
+                add(firstLevelAProcessGroup);
+                add(firstLevelBProcessGroup);
+            }
+        }).when(rootProcessGroup).getProcessGroups();
+
+        Mockito.doReturn(new HashSet<ProcessGroup>() {
+            {
+                add(secondLevelAProcessGroup);
+            }
+        }).when(firstLevelAProcessGroup).getProcessGroups();
+
+        Mockito.doReturn(new HashSet<ProcessGroup>() {
+            {
+                add(secondLevelBProcessGroup);
+            }
+        }).when(firstLevelBProcessGroup).getProcessGroups();
+
+        Mockito.doReturn(new HashSet<ProcessGroup>() {
+            {
+                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<ProcessGroup>() {
+            {
+                add(firstLevelAProcessGroup);
+                add(firstLevelBProcessGroup);
+            }
+        }).when(rootProcessGroup).getProcessGroups();
+
+        Mockito.doReturn(new HashSet<ProcessGroup>() {
+            {
+                add(secondLevelAProcessGroup);
+            }
+        }).when(firstLevelAProcessGroup).getProcessGroups();
+
+        Mockito.doReturn(new HashSet<ProcessGroup>() {
+            {
+                add(secondLevelBProcessGroup);
+            }
+        }).when(firstLevelBProcessGroup).getProcessGroups();
+
+        Mockito.doReturn(new HashSet<ProcessGroup>() {
+            {
+                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<ProcessorNode>() {
+            {
+                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();
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/91e98aa5/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
----------------------------------------------------------------------
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 038a07b..67b9d10 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 = $('<a></a>').append($('<div 
class="search-match-header"></div>').text(match.name));
+                            var itemHeader = $('<div 
class="search-match-header"></div>').text(match.name);
+
+                            var parentGroupHeader = $('<div 
class="search-match-header"></div>').append(document.createTextNode('Parent: 
'));
+                            var parentGroup = match.parentGroup.name ? 
match.parentGroup.name : match.parentGroup.id;
+                            parentGroupHeader = 
parentGroupHeader.append($('<span></span>').text(parentGroup));
+
+                            var versionedGroupHeader = $('<div 
class="search-match-header"></div>').append(document.createTextNode('Versioned: 
'));
+                            var versionedGroup = '-';
+
+                            if 
(nfCommon.isDefinedAndNotNull(match.versionedGroup)) {
+                                versionedGroup = match.versionedGroup.name ? 
match.versionedGroup.name : match.versionedGroup.id;
+                            }
+
+                            versionedGroupHeader = 
versionedGroupHeader.append($('<span></span>').text(versionedGroup));
+                            // create a search item wrapper
+                            var itemContent = 
$('<a></a>').append(itemHeader).append(parentGroupHeader).append(versionedGroupHeader);
+                            // append all matches
                             $.each(match.matches, function (i, match) {
                                 itemContent.append($('<div 
class="search-match"></div>').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();
 

Reply via email to