dk.dma.msinm.service.CategoryService.java Source code

Java tutorial

Introduction

Here is the source code for dk.dma.msinm.service.CategoryService.java

Source

/* Copyright (c) 2011 Danish Maritime Authority
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this library.  If not, see <http://www.gnu.org/licenses/>.
 */
package dk.dma.msinm.service;

import dk.dma.msinm.common.MsiNmApp;
import dk.dma.msinm.common.db.PredicateHelper;
import dk.dma.msinm.common.db.Sql;
import dk.dma.msinm.common.model.DataFilter;
import dk.dma.msinm.common.service.BaseService;
import dk.dma.msinm.model.Category;
import dk.dma.msinm.model.CategoryDesc;
import dk.dma.msinm.vo.CategoryVo;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;

import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Root;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * Business interface for accessing MSI-NM categories
 */
@Stateless
public class CategoryService extends BaseService {

    private final static String FIRING_EXERCISES_CATEGORY = "Firing Exercises";

    @Inject
    private Logger log;

    @Inject
    private MessageService messageService;

    @Inject
    private MsiNmApp app;

    @Inject
    @Sql("/sql/category_messages.sql")
    private String categoryMessagesSql;

    /**
     * Searchs for categories matching the given term in the given language
     * @param lang the language
     * @param term the search term
     * @param limit the maximum number of results
     * @return the search result
     */
    public List<CategoryVo> searchCategories(String lang, String term, int limit) {
        List<CategoryVo> result = new ArrayList<>();
        if (StringUtils.isNotBlank(term)) {
            List<Category> categories = em.createNamedQuery("Category.searchCategories", Category.class)
                    .setParameter("lang", lang).setParameter("term", "%" + term + "%").setParameter("sort", term)
                    .setMaxResults(limit).getResultList();

            DataFilter dataFilter = DataFilter.get(DataFilter.PARENT).setLang(lang);
            categories.forEach(cat -> result.add(new CategoryVo(cat, dataFilter)));
        }
        return result;
    }

    /**
     * Returns the hierarchical list of root categories.
     * <p></p>
     * The returned list is a condensed data set where only description records
     * for the given language is included and no locations are included.
     *
     * @param lang the language
     * @return the hierarchical list of root categories
     */
    public List<CategoryVo> getCategoryTreeForLanguage(String lang) {
        // Ensure validity
        final String language = app.getLanguage(lang);

        // Get all categories along with their CategoryDesc records
        // Will ensure that all Category entities are cached in the entity manager before organizing the result
        List<Category> categories = em.createNamedQuery("Category.findCategoriesWithDescs", Category.class)
                .getResultList();

        // Create a lookup map
        Map<Integer, CategoryVo> categoryLookup = new HashMap<>();
        categories.stream().forEach(category -> categoryLookup.put(category.getId(),
                new CategoryVo(category, DataFilter.get(DataFilter.PARENT_ID).setLang(language))));

        // Add non-roots as child categories to their parent category
        categoryLookup.values().stream().filter(categoryVo -> categoryVo.getParent() != null)
                .forEach(categoryVo -> categoryLookup.get(categoryVo.getParent().getId()).checkCreateChildren()
                        .add(categoryVo));

        // Return roots
        return categoryLookup.values().stream().filter(categoryVo -> categoryVo.getParent() == null)
                .collect(Collectors.toList());
    }

    /**
     * Looks up a category and the associated data, but does NOT look up
     * the child-category hierarchy
     *
     * @param id the id of the category
     * @return the category
     */
    public CategoryVo getCategoryDetails(Integer id) {
        Category category = getByPrimaryKey(Category.class, id);
        if (category == null) {
            return null;
        }

        // NB: No child categories included
        return new CategoryVo(category, DataFilter.get());
    }

    /**
     * Updates the category data from the category template, but not the parent-child hierarchy of the category
     * @param category the category to update
     * @return the updated category
     */
    public Category updateCategoryData(Category category) {
        Category original = getByPrimaryKey(Category.class, category.getId());

        // Copy the category data
        original.copyDescsAndRemoveBlanks(category.getDescs());

        // Update lineage
        original.updateLineage();

        original = saveEntity(original);

        // Evict all cached messages for the category subtree
        evictCachedMessages(original);

        return original;
    }

    /**
     * Creates a new category based on the category template
     * @param category the category to create
     * @param parentId the id of the parent category
     * @return the created category
     */
    public Category createCategory(Category category, Integer parentId) {

        if (parentId != null) {
            Category parent = getByPrimaryKey(Category.class, parentId);
            parent.addChild(category);
        }

        category = saveEntity(category);

        // The category now has an ID - Update lineage
        category.updateLineage();
        category = saveEntity(category);

        em.flush();
        return category;
    }

    /**
     * Moves the category to the given parent id
     * @param categoryId the id of the category to create
     * @param parentId the id of the parent category
     * @return the updated category
     */
    public Category moveCategory(Integer categoryId, Integer parentId) {
        Category category = getByPrimaryKey(Category.class, categoryId);

        if (category.getParent() != null && !category.getParent().getId().equals(parentId)) {
            category.getParent().getChildren().remove(category);
        }

        if (parentId == null) {
            category.setParent(null);
        } else {
            Category parent = getByPrimaryKey(Category.class, parentId);
            parent.addChild(category);
        }

        category = saveEntity(category);
        em.flush();

        // Update all lineages
        updateLineages();

        // Return the update area
        category = getByPrimaryKey(Category.class, category.getId());

        // Evict all cached messages for the category subtree
        evictCachedMessages(category);

        return category;
    }

    /**
     * Evict all cached messages for the given subtree of areas
     * @param category the subtree to evict cached messaged for
     */
    private void evictCachedMessages(Category category) {
        // Sanity check
        if (category == null || category.getLineage() == null) {
            return;
        }

        String sql = categoryMessagesSql.replace(":lineage", "'" + category.getLineage() + "%'");

        List<?> ids = em.createNativeQuery(sql).getResultList();

        ids.forEach(o -> messageService.evictCachedMessageId(((Number) o).intValue()));
    }

    /**
     * Update lineages for all categories
     */
    public void updateLineages() {

        log.info("Update category lineages");

        // Get root areas
        List<Category> roots = getAll(Category.class).stream().filter(Category::isRootCategory)
                .collect(Collectors.toList());

        // Update each root subtree
        List<Category> updated = new ArrayList<>();
        roots.forEach(category -> updateLineages(category, updated));

        // Persist the changes
        updated.forEach(this::saveEntity);
        em.flush();
    }

    /**
     * Recursively updates the lineages of categories rooted at the given category
     * @param category the category whose sub-tree should be updated
     * @param categories the list of updated categories
     * @return if the lineage was updated
     */
    private boolean updateLineages(Category category, List<Category> categories) {

        boolean updated = category.updateLineage();
        if (updated) {
            categories.add(category);
        }
        category.getChildren().forEach(childCategory -> updateLineages(childCategory, categories));
        return updated;
    }

    /**
     * Deletes the category and sub-categories
     * @param categoryId the id of the category to delete
     */
    public boolean deleteCategory(Integer categoryId) {

        Category category = getByPrimaryKey(Category.class, categoryId);
        if (category != null) {
            category.setParent(null);
            saveEntity(category);
            remove(category);
            return true;
        }
        return false;
    }

    /**
     * Looks up an category by name
     * @param name the name to search for
     * @param lang the language. Optional
     * @param parentId the parent ID. Optional
     * @return The matching category, or null if not found
     */
    public Category findByName(String name, String lang, Integer parentId) {
        // Sanity check
        if (StringUtils.isBlank(name)) {
            return null;
        }

        CriteriaBuilder builder = em.getCriteriaBuilder();
        CriteriaQuery<Category> categoryQuery = builder.createQuery(Category.class);

        Root<Category> categoryRoot = categoryQuery.from(Category.class);

        // Build the predicate
        PredicateHelper<Category> predicateBuilder = new PredicateHelper<>(builder, categoryQuery);

        // Match the name
        Join<Category, CategoryDesc> descs = categoryRoot.join("descs", JoinType.LEFT);
        predicateBuilder.like(descs.get("name"), name);
        // Optionally, match the language
        if (StringUtils.isNotBlank(lang)) {
            predicateBuilder.equals(descs.get("lang"), lang);
        }

        // Optionally, match the parent
        if (parentId != null) {
            categoryRoot.join("parent", JoinType.LEFT);
            Path<Category> parent = categoryRoot.get("parent");
            predicateBuilder.equals(parent.get("id"), parentId);
        }

        // Complete the query
        categoryQuery.select(categoryRoot).distinct(true).where(predicateBuilder.where());

        // Execute the query and update the search result
        List<Category> result = em.createQuery(categoryQuery).getResultList();

        return result.size() > 0 ? result.get(0) : null;
    }

    /**
     * Ensures that the template category and it's parents exists
     * @param templateCategory the template category
     * @return the category
     */
    public Category findOrCreateCategory(Category templateCategory) {
        // Sanity checks
        if (templateCategory == null || templateCategory.getDescs().size() == 0) {
            return null;
        }

        // Recursively, resolve the parent categories
        Category parent = null;
        if (templateCategory.getParent() != null) {
            parent = findOrCreateCategory(templateCategory.getParent());
        }
        Integer parentId = (parent == null) ? null : parent.getId();

        // Check if we can find the given category
        Category category = null;
        for (int x = 0; category == null && x < templateCategory.getDescs().size(); x++) {
            CategoryDesc desc = templateCategory.getDescs().get(x);
            category = findByName(desc.getName(), desc.getLang(), parentId);
        }

        // Create the category if no matching category was found
        if (category == null) {
            category = createCategory(templateCategory, parentId);
        }
        return category;
    }

    /**
     * Utility method for getting or creating the firing exercises category
     * @return the firing exercises category
     */
    public Category findOrCreateFiringExercisesCategory() {
        Category category = new Category();
        category.createDesc("en").setName(FIRING_EXERCISES_CATEGORY);
        return findOrCreateCategory(category);
    }

}