io.lavagna.service.SearchService.java Source code

Java tutorial

Introduction

Here is the source code for io.lavagna.service.SearchService.java

Source

/**
 * This file is part of lavagna.
 *
 * lavagna is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * lavagna is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with lavagna.  If not, see <http://www.gnu.org/licenses/>.
 */
package io.lavagna.service;

import static io.lavagna.service.SearchFilter.filter;
import static io.lavagna.service.SearchFilter.filterByColumnDefinition;
import io.lavagna.model.BoardColumn;
import io.lavagna.model.CardFull;
import io.lavagna.model.CardFullWithCounts;
import io.lavagna.model.ColumnDefinition;
import io.lavagna.model.Permission;
import io.lavagna.model.SearchResults;
import io.lavagna.model.UserWithPermission;
import io.lavagna.query.SearchQuery;
import io.lavagna.service.SearchFilter.FilterType;
import io.lavagna.service.SearchFilter.SearchContext;
import io.lavagna.service.SearchFilter.SearchFilterValue;
import io.lavagna.service.SearchFilter.ValueType;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.sql.Clob;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StreamUtils;

/**
 * Service for searching Cards using criterias defined using {@link SearchFilter}.
 */
@Service
@Transactional(readOnly = true)
public class SearchService {

    private static final Logger LOG = LogManager.getLogger();

    private static final int CARDS_PER_PAGE = 50;

    private final NamedParameterJdbcTemplate jdbc;
    private final CardRepository cardRepository;
    private final CardService cardService;
    private final UserRepository userRepository;
    private final ProjectService projectService;
    private final BoardRepository boardRepository;
    private final SearchQuery queries;

    public SearchService(CardRepository cardRepository, CardService cardService, UserRepository userRepository,
            ProjectService projectService, BoardRepository boardRepository, NamedParameterJdbcTemplate jdbc,
            SearchQuery queries) {
        this.cardRepository = cardRepository;
        this.cardService = cardService;
        this.userRepository = userRepository;
        this.projectService = projectService;
        this.boardRepository = boardRepository;
        this.jdbc = jdbc;
        this.queries = queries;
    }

    private List<SearchFilter> filtersAsList(SearchFilter locationFilter, SearchFilter statusOpen,
            boolean excludeArchivedBoards) {
        return excludeArchivedBoards
                ? Arrays.asList(statusOpen, locationFilter,
                        filter(FilterType.BOARD_STATUS, SearchFilter.ValueType.BOOLEAN, Boolean.FALSE))
                : Arrays.asList(statusOpen, locationFilter);
    }

    public Map<ColumnDefinition, Integer> findTaksByColumnDefinition(Integer projectId, Integer boardId,
            boolean excludeArchivedBoards, UserWithPermission user) {
        SearchFilter locationFilter = filter(SearchFilter.FilterType.LOCATION, SearchFilter.ValueType.STRING,
                BoardColumn.BoardColumnLocation.BOARD.toString());

        Map<ColumnDefinition, Integer> results = new EnumMap<>(ColumnDefinition.class);

        SearchFilter statusOpen = filterByColumnDefinition(ColumnDefinition.OPEN);
        SearchResults openTasks = find(filtersAsList(locationFilter, statusOpen, excludeArchivedBoards), projectId,
                boardId, user, 0);
        results.put(ColumnDefinition.OPEN, openTasks.getCount());

        SearchFilter statusClosed = filterByColumnDefinition(ColumnDefinition.CLOSED);
        SearchResults closedTasks = find(filtersAsList(locationFilter, statusClosed, excludeArchivedBoards),
                projectId, boardId, user, 0);
        results.put(ColumnDefinition.CLOSED, closedTasks.getCount());

        SearchFilter statusBacklog = filterByColumnDefinition(ColumnDefinition.BACKLOG);
        SearchResults backlogTasks = find(filtersAsList(locationFilter, statusBacklog, excludeArchivedBoards),
                projectId, boardId, user, 0);
        results.put(ColumnDefinition.BACKLOG, backlogTasks.getCount());

        SearchFilter statusDeferred = filterByColumnDefinition(ColumnDefinition.DEFERRED);
        SearchResults deferredTasks = find(filtersAsList(locationFilter, statusDeferred, excludeArchivedBoards),
                projectId, boardId, user, 0);
        results.put(ColumnDefinition.DEFERRED, deferredTasks.getCount());

        return results;
    }

    public SearchResults find(List<SearchFilter> unmergedSearchFilter, Integer projectId, Integer boardId,
            UserWithPermission currentUser) {
        return find(unmergedSearchFilter, projectId, boardId, currentUser, false, 0);
    }

    public SearchResults find(List<SearchFilter> unmergedSearchFilter, Integer projectId, Integer boardId,
            UserWithPermission currentUser, int page) {
        return find(unmergedSearchFilter, projectId, boardId, currentUser, true, page);
    }

    private SearchResults find(List<SearchFilter> unmergedSearchFilter, Integer projectId, Integer boardId,
            UserWithPermission currentUser, boolean paginate, int page) {

        // if a user don't have access to the specified project id we skip the
        // whole search
        final boolean userHasNotProjectAccess = projectId != null
                && !currentUser.getBasePermissions().containsKey(Permission.READ)
                && !currentUser.projectsWithPermission(Permission.READ)
                        .contains(projectService.findById(projectId).getShortName());
        final boolean userHasNoReadAccess = projectId == null
                && !currentUser.getBasePermissions().containsKey(Permission.READ)
                && currentUser.projectsWithPermission(Permission.READ).isEmpty();

        final boolean noProjectIdForBoardId = projectId == null && boardId != null;

        final boolean boardIsntInProject = projectId != null && boardId != null
                && boardRepository.findBoardById(boardId).getProjectId() != projectId;

        if (userHasNotProjectAccess || userHasNoReadAccess || noProjectIdForBoardId || boardIsntInProject) {
            return new SearchResults(Collections.<CardFullWithCounts>emptyList(), 0, page,
                    paginate ? CARDS_PER_PAGE : Integer.MAX_VALUE, paginate);
        }

        List<SearchFilter> searchFilters = mergeFreeTextFilters(unmergedSearchFilter);
        //

        List<Object> params = new ArrayList<>();
        int filteringConditionsCount = 0;
        //

        List<String> usersOrCardToSearch = new ArrayList<>();
        // fetch all possible user->id, card->id in the value types with string
        // (thus unknown use)
        for (SearchFilter searchFilter : searchFilters) {
            if (searchFilter.getValue() != null && searchFilter.getValue().getType() == ValueType.STRING) {
                usersOrCardToSearch.add(searchFilter.getValue().getValue().toString());
            }
        }
        //

        Map<String, Integer> cardNameToId = cardRepository.findCardsIds(usersOrCardToSearch);
        Map<String, Integer> userNameToId = userRepository.findUsersId(usersOrCardToSearch);

        SearchContext searchContext = new SearchContext(currentUser, userNameToId, cardNameToId);

        //

        StringBuilder baseQuery = new StringBuilder(queries.findFirstFrom()).append("SELECT CARD_ID FROM ( ");

        // add filter conditions
        for (int i = 0; i < searchFilters.size(); i++) {
            SearchFilter searchFilter = searchFilters.get(i);

            String filterConditionQuery = searchFilter.getType().toBaseQuery(searchFilter, queries, params,
                    searchContext);

            baseQuery.append("( ").append(filterConditionQuery).append(" ) ");
            if (i < searchFilters.size() - 1) {
                baseQuery.append(" UNION ALL ");
            }
            filteringConditionsCount++;
        }
        //

        /* AS CARD_IDS -> table alias for mysql */
        baseQuery.append(" ) AS CARD_IDS GROUP BY CARD_ID HAVING COUNT(CARD_ID) = ?").append(queries.findSecond());
        params.add(filteringConditionsCount);

        if (boardId != null) {
            baseQuery.append(queries.findThirdWhere()).append(queries.findFourthInBoardId());
            params.add(boardId);
        } else if (projectId != null) {
            baseQuery.append(queries.findThirdWhere()).append(queries.findInFifthProjectId());
            params.add(projectId);
        } else if (!currentUser.getBasePermissions().containsKey(Permission.READ)) {

            Set<Integer> projectsWithPermission = currentUser.projectsIdWithPermission(Permission.READ);
            baseQuery.append(queries.findThirdWhere()).append(queries.findSixthRestrictedReadAccess()).append(" (")
                    .append(StringUtils.repeat("?", " , ", projectsWithPermission.size())).append(" ) ");

            params.addAll(projectsWithPermission);
        }

        String findCardsQuery = queries.findFirstSelect() + baseQuery.toString() + queries.findSeventhOrderBy();

        if (paginate) {
            params.add(CARDS_PER_PAGE + 1);// limit
            params.add(page * CARDS_PER_PAGE);// offset
            findCardsQuery += queries.findEighthLimit();
        }

        List<Integer> sr = jdbc.getJdbcOperations().queryForList(findCardsQuery, params.toArray(), Integer.class);

        //

        int count = sr.size();
        if (paginate && page == 0 && sr.size() == (CARDS_PER_PAGE + 1) || page > 0) {
            String countCardsQuery = queries.findFirstSelectCount() + baseQuery.toString();
            count = jdbc.getJdbcOperations().queryForObject(countCardsQuery,
                    params.subList(0, params.size() - 2).toArray(), Integer.class);
        }

        //
        return new SearchResults(cardFullWithCounts(sr), count, page, paginate ? CARDS_PER_PAGE : Integer.MAX_VALUE,
                paginate);
    }

    private List<CardFullWithCounts> cardFullWithCounts(List<Integer> sr) {

        if (sr.isEmpty()) {
            return Collections.emptyList();
        }

        // super ugly :(
        Map<Integer, Integer> idToPosition = new HashMap<>();
        for (int i = 0; i < sr.size(); i++) {
            idToPosition.put(sr.get(i), i);
        }

        CardFull[] orderedCf = new CardFull[sr.size()];
        // reorder:
        for (CardFull cf : cardRepository.findAllByIds(sr)) {
            orderedCf[idToPosition.get(cf.getId())] = cf;
        }
        //

        return cardService.fetchCardFull(Arrays.asList(orderedCf));
    }

    private static List<SearchFilter> mergeFreeTextFilters(List<SearchFilter> unmergedSearchFilter) {
        List<SearchFilter> merged = new ArrayList<>(unmergedSearchFilter.size());
        StringBuilder sb = new StringBuilder();
        for (SearchFilter sf : unmergedSearchFilter) {
            if (sf.getType() != FilterType.FREETEXT) {
                merged.add(sf);
            } else {
                sb.append(" ").append(sf.getValue().getValue());
            }
        }

        String freeText = sb.toString().trim();
        if (freeText.length() > 0) {
            merged.add(
                    new SearchFilter(FilterType.FREETEXT, null, new SearchFilterValue(ValueType.STRING, freeText)));
        }

        return merged;
    }

    // used by HSQLDB, obviously not optimized at all (as it's only for dev
    // purpose)
    public static boolean searchText(String data, String toSearch) {
        String[] wordsToSearch = toSearch.split("\\s+");
        String lowerCasedData = data.toLowerCase(Locale.ENGLISH);
        for (String word : wordsToSearch) {
            if (!lowerCasedData.contains(word.toLowerCase(Locale.ENGLISH))) {
                return false;
            }
        }
        return true;
    }

    public static boolean searchTextClob(Clob data, String toSearch) {
        try (InputStream is = data.getAsciiStream()) {
            String res = StreamUtils.copyToString(is, StandardCharsets.UTF_8);
            return searchText(res, toSearch);
        } catch (IOException | SQLException e) {
            LOG.warn("error while reading clob", e);
            return false;
        }
    }
}