private List<AbstractToken> addExpression(final List<AbstractToken> tokens,
                                                  final List<TokenType> consumedTokens,
                                                  final Set<TokenType> allowConsumed,
                                                  final TokenType keyword,
                                                  final Consumer<ExpressionOperator> expressionConsumer) {
            final List<AbstractToken> whereGroup = new ArrayList<>();
            final List<TokenType> localConsumedTokens = new ArrayList<>(consumedTokens);
            final Set<TokenType> allowFollowing = new HashSet<>(allowConsumed);
            int i = 0;
            for (; i < tokens.size(); i++) {
                final AbstractToken token = tokens.get(i);
                if (token instanceof final KeywordGroup keywordGroup) {
                    final TokenType tokenType = keywordGroup.getTokenType();
                    if (keyword.equals(tokenType)) {
                        // Make sure we haven't already consumed the keyword token.
                        checkTokenOrder(token, localConsumedTokens, Set.of(TokenType.FROM), allowConsumed);
                        localConsumedTokens.add(tokenType);
                        consumedTokens.add(tokenType);
                        allowFollowing.add(tokenType);
                        allowFollowing.addAll(TokenType.ALL_LOGICAL_OPERATORS);
                        whereGroup.add(keywordGroup);
                    } else if (TokenType.ALL_LOGICAL_OPERATORS.contains(tokenType)) {
                        // Check we have already consumed the expected keyword token.
                        checkTokenOrder(token, localConsumedTokens, Set.of(TokenType.FROM, keyword), allowFollowing);
                        localConsumedTokens.add(tokenType);
                        whereGroup.add(keywordGroup);
                    } else {
                        break;
                    }
                } else {
                    break;
                }
            }

            if (!whereGroup.isEmpty()) {
                final ExpressionOperator expressionOperator = processLogic(whereGroup);
                // Simplify expression.
                final ExpressionOperator simplifiedExpression = ExpressionUtil.simplify(expressionOperator);
                expressionConsumer.accept(simplifiedExpression);
            } else {
                expressionConsumer.accept(ExpressionOperator.builder().build());
            }

            if (i < tokens.size()) {
                return tokens.subList(i, tokens.size());
            }
            return Collections.emptyList();
        }
@param isLenient Ignores any additional child tokens found. This is to support getting just
                 the data source from a partially written query, which is likely to not be
                 a valid query. For example, you may type
                 <pre>{@code from View sel}</pre> and at this point do code completion on 'sel'
                 so if this method is called it will treat 'sel as an extra child.
stroom-query/stroom-query-common/src/main/java/stroom/query/language/SearchRequestFactory.java