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

Java tutorial

Introduction

Here is the source code for dk.dma.msinm.service.AreaService.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.common.settings.DefaultSetting;
import dk.dma.msinm.common.settings.Setting;
import dk.dma.msinm.common.settings.Settings;
import dk.dma.msinm.common.settings.SettingsEntity;
import dk.dma.msinm.model.Area;
import dk.dma.msinm.model.AreaDesc;
import dk.dma.msinm.vo.AreaVo;
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.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

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

    // Used by the recomputeTreeSortOrder() method
    public static final Setting AREA_LAST_UPDATE = new DefaultSetting("areaLastUpdate", "0");

    @Inject
    private Logger log;

    @Inject
    MessageService messageService;

    @Inject
    private MsiNmApp app;

    @Inject
    @Sql("/sql/area_messages.sql")
    private String areaMessagesSql;

    @Inject
    private Settings settings;

    /**
     * Searchs for areas 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<AreaVo> searchAreas(String lang, String term, int limit) {
        List<AreaVo> result = new ArrayList<>();
        if (StringUtils.isNotBlank(term)) {
            List<Area> areas = em.createNamedQuery("Area.searchAreas", Area.class).setParameter("lang", lang)
                    .setParameter("term", "%" + term + "%").setParameter("sort", term).setMaxResults(limit)
                    .getResultList();

            DataFilter dataFilter = DataFilter.get(DataFilter.PARENT).setLang(lang);
            areas.forEach(area -> result.add(new AreaVo(area, dataFilter)));
        }
        return result;
    }

    /**
     * Returns the hierarchical list of root areas.
     * <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 areas
     */
    public List<AreaVo> getAreaTreeForLanguage(String lang) {
        // Ensure validity
        final String language = app.getLanguage(lang);

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

        // Create a lookup map
        //Map<Integer, AreaVo> areaLookup = areas.stream()
        //        .collect(Collectors.toMap(Area::getId, area -> new AreaVo(area, language)));
        Map<Integer, AreaVo> areaLookup = new HashMap<>();
        areas.stream().forEach(area -> areaLookup.put(area.getId(),
                new AreaVo(area, DataFilter.get(DataFilter.PARENT_ID).setLang(language))));

        // Add non-roots as child areas to their parent area
        areaLookup.values().stream().filter(areaVo -> areaVo.getParent() != null)
                .forEach(areaVo -> areaLookup.get(areaVo.getParent().getId()).checkCreateChildren().add(areaVo));

        // Return roots
        List<AreaVo> roots = areaLookup.values().stream().filter(areaVo -> areaVo.getParent() == null)
                .collect(Collectors.toList());

        // Sort the trees according to sort order
        Collections.sort(roots);
        roots.forEach(AreaVo::sortChildren);

        return roots;
    }

    /**
     * Looks up an area and the associated data, but does NOT look up
     * the child-area hierarchy
     *
     * @param id the id of the area
     * @return the area
     */
    public AreaVo getAreaDetails(Integer id) {
        Area area = getByPrimaryKey(Area.class, id);
        if (area == null) {
            return null;
        }

        // NB: No child areas included
        return new AreaVo(area, DataFilter.get("locations"));
    }

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

        original.setSortOrder(area.getSortOrder());

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

        // Add the locations
        original.getLocations().clear();
        original.getLocations().addAll(area.getLocations());

        // Update lineage
        original.updateLineage();

        original = saveEntity(original);

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

        return original;
    }

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

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

        area = saveEntity(area);

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

        em.flush();
        return area;
    }

    /**
     * Moves the area to the given parent id
     * @param areaId the id of the area to create
     * @param parentId the id of the parent area
     * @return the updated area
     */
    public Area moveArea(Integer areaId, Integer parentId) {
        Area area = getByPrimaryKey(Area.class, areaId);

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

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

        // Save the entity
        area = saveEntity(area);
        em.flush();

        // Update all lineages
        updateLineages();

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

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

        return area;
    }

    /**
     * Changes the sort order of an area, by moving it up or down compared to siblings.
     * <p>
     * Please note that by moving "up" we mean in a geographical tree structure,
     * i.e. a smaller sortOrder value.
     *
     * @param areaId the id of the area to move
     * @param moveUp whether to move the area up or down
     * @return the updated area
     */
    public Area changeSortOrder(Integer areaId, boolean moveUp) {
        Area area = getByPrimaryKey(Area.class, areaId);
        boolean updated = false;

        // Non-root case
        if (area.getParent() != null) {
            List<Area> siblings = area.getParent().getChildren();
            int index = siblings.indexOf(area);

            if (moveUp) {
                if (index == 1) {
                    area.setSortOrder(siblings.get(0).getSortOrder() - 10.0);
                    updated = true;
                } else if (index > 1) {
                    double so1 = siblings.get(index - 1).getSortOrder();
                    double so2 = siblings.get(index - 2).getSortOrder();
                    area.setSortOrder((so1 + so2) / 2.0);
                    updated = true;
                }

            } else {
                if (index == siblings.size() - 2) {
                    area.setSortOrder(siblings.get(siblings.size() - 1).getSortOrder() + 10.0);
                    updated = true;
                } else if (index < siblings.size() - 2) {
                    double so1 = siblings.get(index + 1).getSortOrder();
                    double so2 = siblings.get(index + 2).getSortOrder();
                    area.setSortOrder((so1 + so2) / 2.0);
                    updated = true;
                }

            }

        } else {
            // TODO root case
        }

        if (updated) {
            log.info("Updates sort order for area " + area.getId() + " to " + area.getSortOrder());
            // Save the entity
            area = saveEntity(area);

            // NB: Cache eviction not needed since lineage is the same...
        }

        return area;
    }

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

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

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

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

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

        log.info("Update area lineages");

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

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

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

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

        boolean updated = area.updateLineage();
        if (updated) {
            areas.add(area);
        }
        area.getChildren().forEach(childArea -> updateLineages(childArea, areas));
        return updated;
    }

    /**
     * Deletes the area and sub-areas
     * @param areaId the id of the area to delete
     */
    public boolean deleteArea(Integer areaId) {

        Area area = getByPrimaryKey(Area.class, areaId);
        if (area != null) {
            area.setParent(null);
            saveEntity(area);
            remove(area);
            return true;
        }
        return false;
    }

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

        CriteriaBuilder builder = em.getCriteriaBuilder();
        CriteriaQuery<Area> areaQuery = builder.createQuery(Area.class);

        Root<Area> areaRoot = areaQuery.from(Area.class);

        // Build the predicate
        PredicateHelper<Area> predicateBuilder = new PredicateHelper<>(builder, areaQuery);

        // Match the name
        Join<Area, AreaDesc> descs = areaRoot.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) {
            areaRoot.join("parent", JoinType.LEFT);
            Path<Area> parent = areaRoot.get("parent");
            predicateBuilder.equals(parent.get("id"), parentId);
        }

        // Complete the query
        areaQuery.select(areaRoot).distinct(true).where(predicateBuilder.where());

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

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

    /**
     * Ensures that the template area and it's parents exists
     * @param templateArea the template area
     * @return the area
     */
    public Area findOrCreateArea(Area templateArea) {
        // Sanity checks
        if (templateArea == null || templateArea.getDescs().size() == 0) {
            return null;
        }

        // Recursively, resolve the parent areas
        Area parent = null;
        if (templateArea.getParent() != null) {
            parent = findOrCreateArea(templateArea.getParent());
        }
        Integer parentId = (parent == null) ? null : parent.getId();

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

        // Create the area if no matching area was found
        if (area == null) {
            area = createArea(templateArea, parentId);
        }
        return area;
    }

    /**
     * Returns the last change date for areas or null if no area exists
     * @return the last change date for areas
     */
    public Date getLastUpdated() {
        try {
            return em.createQuery("select max(a.updated) from Area a", Date.class).getSingleResult();
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * Potentially, a heavy-duty function that scans the entire area tree,
     * sorts it and update the treeSortOrder. Use with care.
     */
    public void recomputeTreeSortOrder() {
        long t0 = System.currentTimeMillis();

        // Compare the last area update date and the last processed date
        Date lastAreaUpdate = getLastUpdated();
        if (lastAreaUpdate == null) {
            // No areas
            return;
        }

        Date lastProcessedUpdate = settings.getDate(AREA_LAST_UPDATE);
        if (!lastAreaUpdate.after(lastProcessedUpdate)) {
            log.debug("No area tree changes since last execution of recomputeTreeSortOrder()");
            return;
        }

        List<Area> roots = em.createNamedQuery("Area.findRootAreas", Area.class).getResultList();

        // Sort the roots by sortOrder
        Collections.sort(roots);

        // Re-compute the tree sort order
        List<Area> updated = new ArrayList<>();
        recomputeTreeSortOrder(roots, 0, updated, false);

        // Persist changed areas
        updated.forEach(this::saveEntity);

        em.flush();

        // Update the last processed date
        settings.updateSetting(new SettingsEntity(AREA_LAST_UPDATE.getSettingName(),
                String.valueOf(System.currentTimeMillis() + 1000)));

        log.info("Recomputed tree sort order in " + (System.currentTimeMillis() - t0) + " ms");
    }

    /**
     * Recursively recomputes the treeSortOrder, by enumerating the sorted area list and their children
     * @param areas the list of areas to update
     * @param index the current area index
     * @param updatedAreas the list of updated areas given by sub-tree roots.
     * @param ancestorUpdated if an ancestor area has been updated
     * @return the index after processing the list of areas.
     */
    private int recomputeTreeSortOrder(List<Area> areas, int index, List<Area> updatedAreas,
            boolean ancestorUpdated) {

        for (Area area : areas) {
            index++;
            boolean updated = ancestorUpdated;
            if (index != area.getTreeSortOrder()) {
                area.setTreeSortOrder(index);
                updated = true;
                if (!ancestorUpdated) {
                    updatedAreas.add(area);
                }
            }

            // NB: area.getChildren() is by definition sorted
            index = recomputeTreeSortOrder(area.getChildren(), index, updatedAreas, updated);
        }

        return index;
    }

}