private Predicate<FilterableNode> buildCombinedPredicate(final ExplorerTreeFilter filter) {
        final Predicate<FilterableNode> combinedPredicate;
        final List<Predicate<FilterableNode>> predicates = new ArrayList<>(4);

        boolean foundAlwaysFalsePredicate = false;
        final Set<String> includedTypes = filter.getIncludedTypes();

        if (includedTypes != null) {
            if (includedTypes.isEmpty()) {
                LOGGER.debug("Always false triggered by checkType");
                foundAlwaysFalsePredicate = true;
            } else {
                LOGGER.debug("Adding checkType predicate for types: {}", includedTypes);
                predicates.add(this::checkType);
            }
        }

        final Set<String> filterTags = NullSafe.set(filter.getTags());
        if (!foundAlwaysFalsePredicate && !filterTags.isEmpty()) {
            LOGGER.debug("Adding checkTags predicate for tags: {}", filterTags);
            predicates.add(filterableNode ->
                    filterableNode.node.hasTags(filterTags));
        }

        final Set<NodeFlag> filterNodeFlags = NullSafe.set(filter.getNodeFlags());
        if (!foundAlwaysFalsePredicate && !filterNodeFlags.isEmpty()) {
            LOGGER.debug("Adding checkNodeFlags predicate for nodeFlags: {}", filterNodeFlags);
            predicates.add(filterableNode ->
                    filterableNode.node().hasNodeFlags(filterNodeFlags));
        }

        if (!foundAlwaysFalsePredicate) {
            final String nameFilter = filter.getNameFilter();
            if (!NullSafe.isBlankString(nameFilter)) {
                LOGGER.debug("Adding testWithNameFilter predicate for nameFilter: '{}'", nameFilter);
                // Ensure the predicate is initialised
                getFuzzyMatchPredicate();
                predicates.add(this::testWithNameFilter);
            }
        }

        if (!foundAlwaysFalsePredicate) {
            final Set<DocumentPermission> requiredPermissions = filter.getRequiredPermissions();
            if (NullSafe.isEmptyCollection(requiredPermissions)) {
                LOGGER.debug("Always false triggered by hasPermission");
                foundAlwaysFalsePredicate = true;
            } else {
                LOGGER.debug("Adding hasPermission predicate for requiredPermissions: {}", requiredPermissions);
                predicates.add(this::hasPermission);
            }
        }

        if (foundAlwaysFalsePredicate) {
            LOGGER.debug("Using always false predicate");
            combinedPredicate = ALWAYS_FALSE_PREDICATE;
        } else {
            LOGGER.debug(() -> LogUtil.message("Using combined predicate containing {} predicate{}",
                    predicates.size(), StringUtil.pluralSuffix(predicates.size())));
            combinedPredicate = PredicateUtil.andPredicates(predicates, ALWAYS_FALSE_PREDICATE);
        }
        return combinedPredicate;
    }
Some docRefs will get tested multiple times over the course of building the output tree
so hold a transient cache of filter matches. Also creates a combined predicate that only
includes the required checks bases on what is set in the ExplorerTreeFilter.
stroom-explorer/stroom-explorer-impl/src/main/java/stroom/explorer/impl/NodeInclusionChecker.java