org.libreplan.web.resources.search.NewAllocationSelectorController.java Source code

Java tutorial

Introduction

Here is the source code for org.libreplan.web.resources.search.NewAllocationSelectorController.java

Source

/*
 * This file is part of LibrePlan
 *
 * Copyright (C) 2009-2010 Fundacin para o Fomento da Calidade Industrial e
 *                         Desenvolvemento Tecnolxico de Galicia
 * Copyright (C) 2010-2011 Igalia, S.L.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.libreplan.web.resources.search;

import static org.libreplan.web.I18nHelper._;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.commons.lang3.Validate;
import org.joda.time.LocalDate;
import org.libreplan.business.common.IOnTransaction;
import org.libreplan.business.resources.daos.IResourceLoadRatiosCalculator.ILoadRatiosDataType;
import org.libreplan.business.resources.daos.IResourcesSearcher.IResourcesQuery;
import org.libreplan.business.resources.entities.Criterion;
import org.libreplan.business.resources.entities.CriterionType;
import org.libreplan.business.resources.entities.Resource;
import org.libreplan.business.resources.entities.ResourceType;
import org.libreplan.web.common.Util;
import org.libreplan.web.common.components.NewAllocationSelector.AllocationType;
import org.libreplan.web.common.components.ResourceAllocationBehaviour;
import org.libreplan.web.planner.allocation.INewAllocationsAdder;
import org.zkoss.lang.Objects;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.WrongValueException;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.event.InputEvent;
import org.zkoss.zul.Datebox;
import org.zkoss.zul.Listbox;
import org.zkoss.zul.Radiogroup;
import org.zkoss.zul.Tree;
import org.zkoss.zul.Radio;
import org.zkoss.zul.Listheader;
import org.zkoss.zul.SimpleListModel;
import org.zkoss.zul.Listitem;
import org.zkoss.zul.Treeitem;
import org.zkoss.zul.DefaultTreeNode;
import org.zkoss.zul.DefaultTreeModel;
import org.zkoss.zul.ListitemRenderer;
import org.zkoss.zul.Listcell;
import org.zkoss.zul.Div;
import org.zkoss.zul.Image;
import org.zkoss.zul.Label;
import org.zkoss.zul.Constraint;
import org.zkoss.zul.TreeModel;
import org.zkoss.zul.Treecell;
import org.zkoss.zul.Treerow;
import org.zkoss.zul.TreeitemRenderer;

/**
 * Controller for searching for {@link Resource}.
 *
 * @author Diego Pino Garcia <dpino@igalia.com>
 * @author Javier Moran Rua <jmoran@igalia.com>
 *
 */
public class NewAllocationSelectorController extends AllocationSelectorController {

    private static final BigDecimal AVAILABILITY_GOOD_VALUE = new BigDecimal(0.50);

    private static final BigDecimal AVAILABILITY_INTERMEDIUM_VALUE = new BigDecimal(0.25);

    private ResourceListRenderer resourceListRenderer = new ResourceListRenderer();

    private Radiogroup allocationTypeSelector;

    private Tree criterionsTree;

    private Listbox listBoxResources;

    private Label allocationSelectedItems;

    private Datebox startDateLoadRatiosDatebox, endDateLoadRatiosDatebox;

    private CriterionRenderer criterionRenderer = new CriterionRenderer();

    private AllocationType currentAllocationType;

    private ResourceAllocationBehaviour behaviour;

    public NewAllocationSelectorController(ResourceAllocationBehaviour behaviour) {
        this.behaviour = behaviour;
    }

    @Override
    public void doAfterCompose(Component comp) throws Exception {
        super.doAfterCompose(comp);
        comp.setAttribute("selectorController", this, true);
        initializeComponents();
    }

    private void initializeComponents() {
        initializeCriteriaTree();
        initializeListboxResources();
        initializeAllocationTypeSelector();
        initializeFilteringDatesConstraints();
    }

    private void initializeFilteringDatesConstraints() {
        startDateLoadRatiosDatebox.setConstraint(checkConstraintFilteringDate());
        endDateLoadRatiosDatebox.setConstraint(checkConstraintFilteringDate());
    }

    private void initializeCriteriaTree() {
        // Initialize criteria tree
        if (criterionsTree != null) {
            criterionsTree.addEventListener("onSelect", event -> {
                searchResources("", getSelectedCriterions());
                showSelectedAllocations();
            });
        }

        criterionsTree.setItemRenderer(criterionRenderer);
    }

    private void initializeListboxResources() {
        // Initialize found resources box
        listBoxResources.addEventListener(Events.ON_SELECT, event -> {
            if (isGenericType()) {
                returnToSpecificDueToResourceSelection();
            }
            showSelectedAllocations();
        });

        listBoxResources.setMultiple(behaviour.allowMultipleSelection());
        listBoxResources.setItemRenderer(getListitemRenderer());
    }

    private void initializeAllocationTypeSelector() {
        // Add onCheck listener
        listBoxResources.setItemRenderer(getListitemRenderer());

        // Initialize radio group of selector types
        allocationTypeSelector.addEventListener(Events.ON_CHECK, event -> {
            Radio radio = (Radio) event.getTarget();

            if (radio == null) {
                return;
            }

            onType(AllocationType.valueOf(radio.getValue()));
            showSelectedAllocations();
        });

        // Feed with values
        for (AllocationType each : behaviour.allocationTypes()) {
            allocationTypeSelector.appendChild(radio(each));
        }
        doInitialSelection();
    }

    private Radio radio(AllocationType allocationType) {
        Radio result = new Radio(allocationType.getName());
        result.setValue(allocationType.toString());
        return result;
    }

    private void onType(AllocationType type) {
        currentAllocationType = type;
        Util.reloadBindings(criterionsTree);
        refreshListBoxResources();
    }

    private void doInitialSelection() {
        Radio item = allocationTypeSelector.getItemAtIndex(0);
        currentAllocationType = AllocationType.valueOf(item.getValue());
        showSelectedAllocations();
        item.setSelected(true);
    }

    private void showSelectedAllocations() {
        allocationSelectedItems.setValue(buildSelectedAllocationsString());
    }

    private String buildSelectedAllocationsString() {
        if (currentAllocationType == AllocationType.SPECIFIC) {
            List<Resource> resources = getSelectedResources();

            return Resource.getCaptionFor(resources);
        } else {
            List<Criterion> criteria = getSelectedCriterions();

            return currentAllocationType.asCaption(criteria);
        }
    }

    private List<ResourceWithItsLoadRatios> getAllResources() {
        return addLoadRatiosCalculations(query().byResourceType(getType()).execute());
    }

    private List<ResourceWithItsLoadRatios> addLoadRatiosCalculations(List<? extends Resource> listResources) {

        List<ResourceWithItsLoadRatios> result = new ArrayList<>();

        for (Resource each : listResources) {
            ILoadRatiosDataType t = resourceLoadRatiosCalculator.calculateLoadRatios(each,
                    LocalDate.fromDateFields(startDateLoadRatiosDatebox.getValue()),
                    LocalDate.fromDateFields(endDateLoadRatiosDatebox.getValue()), scenarioManager.getCurrent());

            result.add(new ResourceWithItsLoadRatios(each, t));
        }

        return result;
    }

    private ResourceType getType() {
        return behaviour.getType();
    }

    private IResourcesQuery<?> query() {
        return currentAllocationType.doQueryOn(resourcesSearcher);
    }

    private void refreshListBoxResources() {
        refreshListBoxResources(getAllResources());
    }

    private void refreshListBoxResources(List<ResourceWithItsLoadRatios> resources) {
        listBoxResources.setModel(new SimpleListModel<>(resources));
        triggerSortListBoxResources();
    }

    private void triggerSortListBoxResources() {
        for (Object child : listBoxResources.getListhead().getChildren()) {
            final Listheader hd = (Listheader) child;
            if (!"natural".equals(hd.getSortDirection())) {
                hd.sort("ascending".equals(hd.getSortDirection()), true);
            }
        }
    }

    private void returnToSpecificDueToResourceSelection() {
        currentAllocationType = AllocationType.SPECIFIC;
        List<Criterion> criteria = getSelectedCriterions();
        List<Resource> selectedWorkers = getSelectedWorkers();
        refreshListBoxResources(
                addLoadRatiosCalculations(query().byCriteria(criteria).byResourceType(getType()).execute()));

        listBoxResources.renderAll();
        // Force render so list items has the value property so the resources can be selected

        selectWorkers(selectedWorkers);
        currentAllocationType.doTheSelectionOn(allocationTypeSelector);
    }

    private void selectWorkers(Collection<? extends Resource> selectedWorkers) {
        for (Resource each : selectedWorkers) {
            Listitem listItem = findListItemFor(each);

            if (listItem != null) {
                listItem.setSelected(true);
            }
        }
    }

    private Listitem findListItemFor(Resource resource) {
        @SuppressWarnings("unchecked")
        Collection<Listitem> items = listBoxResources.getItems();
        for (Listitem item : items) {
            Resource itemResource = ((ResourceWithItsLoadRatios) item.getValue()).getResource();

            if (itemResource != null && itemResource.getId().equals(resource.getId())) {
                return item;
            }
        }

        return null;
    }

    private static final EnumSet<AllocationType> genericTypes = EnumSet.of(AllocationType.GENERIC_MACHINES,
            AllocationType.GENERIC_WORKERS);

    private boolean isGenericType() {
        return genericTypes.contains(currentAllocationType);
    }

    @SuppressWarnings("unchecked")
    private void clearSelection(Listbox listBox) {
        Set<Listitem> selectedItems = new HashSet<>(listBox.getSelectedItems());
        for (Listitem each : selectedItems) {
            listBox.removeItemFromSelection(each);
        }
    }

    @SuppressWarnings("unchecked")
    private void clearSelection(Tree tree) {
        Set<Treeitem> selectedItems = new HashSet<>(tree.getSelectedItems());
        for (Treeitem each : selectedItems) {
            tree.removeItemFromSelection(each);
        }
    }

    /**
     * Get input text, and search for workers.
     *
     * @param event
     */
    public void searchWorkers(InputEvent event) {
        searchResources(event.getValue(), getSelectedCriterions());
    }

    /**
     * Does the actual search for workers, and refresh results.
     *
     * @param name
     * @param criterions
     */
    private void searchResources(String name, List<Criterion> criterions) {

        final List<? extends Resource> resources = query().byName(name).byCriteria(criterions)
                .byResourceType(getType()).execute();

        refreshListBoxResources(addLoadRatiosCalculations(resources));
    }

    /**
     * Returns list of selected {@link Criterion}, selects only those which are leaf nodes.
     *
     * @return {@link List<Criterion>}
     */
    @Override
    public List<Criterion> getSelectedCriterions() {
        List<Criterion> result = new ArrayList<>();

        Set<Treeitem> selectedItems = criterionsTree.getSelectedItems();
        for (Treeitem item : selectedItems) {
            CriterionTreeNode node = item.getValue();

            if (node.getData() instanceof Criterion) {
                result.add((Criterion) node.getData());
            }
        }

        return result;
    }

    public ResourceListRenderer getListitemRenderer() {
        return resourceListRenderer;
    }

    @Override
    public void onClose() {
        clearAll();
    }

    @Override
    public void clearAll() {
        /* Do nothing */
    }

    public void open(final LocalDate start, final LocalDate end) {
        setStartFilteringDate(start);
        setEndFilteringDate(end);

        clearSelection(listBoxResources);
        clearSelection(criterionsTree);

        adHocTransactionService.runOnReadOnlyTransaction((IOnTransaction<Void>) () -> {
            refreshListBoxResources();
            criterionsTree.setModel(getCriterions());

            return null;
        });

        doInitialSelection();
    }

    public List<Resource> getSelectedWorkers() {
        if (isGenericType()) {
            return allResourcesShown();
        } else {
            return getSelectedResources();
        }
    }

    @SuppressWarnings("unchecked")
    private List<Resource> allResourcesShown() {
        List<Resource> result = new ArrayList<>();
        List<Listitem> selectedItems = listBoxResources.getItems();
        for (Listitem item : selectedItems) {
            result.add(((ResourceWithItsLoadRatios) item.getValue()).getResource());
        }

        return result;
    }

    @SuppressWarnings("unchecked")
    private List<Resource> getSelectedResources() {
        List<Resource> result = new ArrayList();
        Set<Listitem> selectedItems = listBoxResources.getSelectedItems();

        for (Listitem item : selectedItems) {

            result.add(((ResourceWithItsLoadRatios) item.getValue()).getResource());
        }

        return result;
    }

    /**
     * Encapsulates {@link DefaultTreeNode}.
     *
     * @author Diego Pino Garca <dpino@igalia.com>
     */
    private static class CriterionTreeNode extends DefaultTreeNode {

        public CriterionTreeNode(Object data, List<CriterionTreeNode> children) {
            super(data, children);
        }

        /**
         * Returns {@link CriterionTreeNode} name depending if node contains a {@link Criterion} or a {@link CriterionType}.
         */
        @Override
        public String toString() {
            if (getData() instanceof CriterionType) {
                return ((CriterionType) getData()).getName();
            }
            if (getData() instanceof Criterion) {
                return ((Criterion) getData()).getName();
            }

            return "";
        }
    }

    /**
     * Gets all {@link Criterion} and returns a {@link TreeModel} out of it.
     * This {@link TreeModel} is used to feed criterionsTree widget.
     *
     * @return {@link TreeModel}
     */
    public TreeModel getCriterions() {
        Map<CriterionType, Set<Criterion>> criterions = query().getCriteria();

        List<CriterionTreeNode> rootList = new ArrayList<>();
        for (Entry<CriterionType, Set<Criterion>> entry : criterions.entrySet()) {
            rootList.add(asNode(entry.getKey(), entry.getValue()));
        }
        CriterionTreeNode root = new CriterionTreeNode("Root", rootList);

        return new DefaultTreeModel(root);
    }

    public List<AllocationType> getAllocationTypes() {
        return Arrays.asList(AllocationType.values());
    }

    /**
     * Converts {@link CriterionType} to {@link CriterionTreeNode}.
     *
     * @param criterionType
     * @param criterions
     * @return {@link CriterionTreeNode}
     */
    private CriterionTreeNode asNode(CriterionType criterionType, Set<Criterion> criterions) {
        return new CriterionTreeNode(criterionType, toNodeList(withoutParents(criterions)));
    }

    private List<Criterion> withoutParents(Collection<? extends Criterion> criterions) {
        List<Criterion> result = new ArrayList<>();
        for (Criterion each : criterions) {
            if (each.getParent() == null) {
                result.add(each);
            }
        }

        return result;
    }

    private List<CriterionTreeNode> toNodeList(Collection<? extends Criterion> criterions) {
        ArrayList<CriterionTreeNode> result = new ArrayList<>();
        for (Criterion criterion : sortedByName(criterions)) {
            result.add(asNode(criterion));
        }

        return result;
    }

    private List<Criterion> sortedByName(Collection<? extends Criterion> criterions) {
        List<Criterion> result = new ArrayList<>(criterions);
        Collections.sort(result, Criterion.byName);

        return result;
    }

    /**
     * Converts {@link Criterion} to {@link CriterionTreeNode}.
     *
     * @param criterion
     * @return {@link CriterionTreeNode}
     */
    private CriterionTreeNode asNode(Criterion criterion) {
        return new CriterionTreeNode(criterion, toNodeList(criterion.getChildren()));
    }

    private static class ResourceWithItsLoadRatios implements Comparable<ResourceWithItsLoadRatios> {

        private Resource resource;

        private ILoadRatiosDataType ratios;

        public ResourceWithItsLoadRatios(Resource resource, ILoadRatiosDataType ratios) {
            Validate.notNull(resource);
            Validate.notNull(ratios);
            this.resource = resource;
            this.ratios = ratios;
        }

        public Resource getResource() {
            return this.resource;
        }

        public ILoadRatiosDataType getRatios() {
            return this.ratios;
        }

        @Override
        public int compareTo(ResourceWithItsLoadRatios o) {
            return this.resource.compareTo(o.getResource());
        }
    }

    private static class ResourceListRenderer implements ListitemRenderer {

        /**
         * Render for listBoxResources.
         *
         * @author Diego Pino Garca <dpino@igalia.com>
         */
        @Override
        public void render(Listitem listitem, Object o, int i) throws Exception {
            listitem.setValue(o);
            appendLabelResource(listitem);
        }

        private void appendLabelResource(Listitem item) {
            ResourceWithItsLoadRatios dataToRender = item.getValue();

            Listcell cellName = new Listcell();
            Resource resource = dataToRender.getResource();
            cellName.appendChild(new Label(resource.getShortDescription()));
            item.appendChild(cellName);

            Listcell cellAvailability = new Listcell();
            BigDecimal availability = dataToRender.getRatios().getAvailiabilityRatio();
            Div totalDiv = new Div();
            totalDiv.setStyle("width:50px;height:12px;border: solid 1px black");
            Div containedDiv = new Div();

            String styleValue = "width:" + availability.movePointRight(2) + "%;height:12px;background-color:"
                    + calculateRgba(availability) + ";float:left;left:0";
            containedDiv.setStyle(styleValue);

            Label l = new Label(availability.movePointRight(2).toString() + "%");
            l.setStyle("width:50px;margin-left: 12px");
            containedDiv.appendChild(l);
            totalDiv.appendChild(containedDiv);
            cellAvailability.appendChild(totalDiv);
            item.appendChild(cellAvailability);

            Listcell cellOvertime = new Listcell();
            BigDecimal overtime = dataToRender.getRatios().getOvertimeRatio();
            Label overtimeLabel = new Label(overtime.toString());
            cellOvertime.appendChild(overtimeLabel);
            if (!overtime.equals(BigDecimal.ZERO.setScale(2))) {
                overtimeLabel.setStyle("position: relative; top: -12px");
                Image img = new Image("/dashboard/img/value-meaning-negative.png");
                img.setStyle("width: 25px; position: relative; top: -5px");
                cellOvertime.appendChild(img);
            }

            item.appendChild(cellOvertime);
        }

        private String calculateRgba(BigDecimal availability) {
            String result;
            if (availability.compareTo(AVAILABILITY_INTERMEDIUM_VALUE) < 0) {
                result = "rgba(150,0,0,0.3)";
            } else if (availability.compareTo(AVAILABILITY_GOOD_VALUE) < 0) {
                result = "rgba(255,255,0,0.5)";
            } else {
                result = "rgba(102,204,0,0.3)";
            }

            return result;
        }
    }

    public CriterionRenderer getCriterionRenderer() {
        return criterionRenderer;
    }

    /**
     * Render for criterionsTree.
     *
     * I had to implement a renderer for the Tree.
     * Every item in the tree should be set as opened at first.
     * I tried to do this by iterating through the list of items after setting the model at doAfterCompose,
     * but I got a {@link java.util.ConcurrentModificationException}.
     * It seems that at that point some other component was using the list of items, so it was not possible to modify it.
     * There's not other point where to initialize components but doAfterCompose.
     *
     * @author Diego Pino Garcia <dpino@igalia.com>
     */
    private static class CriterionRenderer implements TreeitemRenderer {

        /**
         * Copied verbatim from org.zkoss.zul.Tree
         */
        @Override
        public void render(Treeitem treeitem, Object o, int i) throws Exception {
            Treecell tc = new Treecell(Objects.toString(o));
            Treerow tr;
            treeitem.setValue(o);
            if (treeitem.getTreerow() == null) {
                tr = new Treerow();
                tr.setParent(treeitem);
                treeitem.setOpen(true); // Expand node
            } else {
                tr = treeitem.getTreerow();
                tr.getChildren().clear();
            }
            tc.setParent(tr);
        }
    }

    @Override
    public void addTo(INewAllocationsAdder allocationsAdder) {
        currentAllocationType.addTo(this, allocationsAdder);
    }

    public void allowSelectMultipleResources(boolean multiple) {
        listBoxResources.setMultiple(multiple);
    }

    public boolean isAllowSelectMultipleResources() {
        return listBoxResources.isMultiple();
    }

    public void setEndFilteringDate(LocalDate d) {
        endDateLoadRatiosDatebox.setValue(asDate(d));
    }

    public void setStartFilteringDate(LocalDate date) {
        startDateLoadRatiosDatebox.setValue(asDate(date));
    }

    private static Date asDate(LocalDate date) {
        if (date == null) {
            return null;
        }

        return date.toDateTimeAtStartOfDay().toDate();
    }

    public void updateLoadRatios() {
        searchResources("", getSelectedCriterions());
    }

    public Constraint checkConstraintFilteringDate() {
        return (comp, value) -> {
            if (value == null) {
                if (comp.getId().equals("startDateLoadRatiosDatebox")) {
                    throw new WrongValueException(comp, _("Start filtering date cannot be empty"));
                } else if (comp.getId().equals("endDateLoadRatiosDatebox")) {
                    throw new WrongValueException(comp, _("End filtering date cannot be empty"));
                }
            }

            Date startDate;

            if (comp.getId().equals("startDateLoadRatiosDatebox")) {
                startDate = (Date) value;
            } else {
                startDate = (Date) startDateLoadRatiosDatebox.getRawValue();
            }

            Date endDate;
            if (comp.getId().equals("endDateLoadRatiosDatebox")) {
                endDate = (Date) value;
            } else {
                endDate = (Date) endDateLoadRatiosDatebox.getRawValue();
            }

            if ((startDate != null) && (endDate != null)) {
                if ((startDate.after(endDate)) || (startDate.equals(endDate))) {
                    throw new WrongValueException(comp,
                            _("Start filtering date must be before than end filtering date"));
                }
            }
        };
    }

}