services.plugins.redmine.RedminePluginRunner.java Source code

Java tutorial

Introduction

Here is the source code for services.plugins.redmine.RedminePluginRunner.java

Source

/*! LICENSE
 *
 * Copyright (c) 2015, The Agile Factory SA and/or its affiliates. All rights
 * reserved.
 *
 * This program 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; version 2 of the License.
 *
 * 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 General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see <http://www.gnu.org/licenses/>.
 */
package services.plugins.redmine;

import java.io.IOException;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.TimeUnit;

import org.apache.commons.configuration.ConfigurationConverter;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.apache.commons.lang3.tuple.Pair;

import com.taskadapter.redmineapi.RedmineException;
import com.taskadapter.redmineapi.RedmineManager;
import com.taskadapter.redmineapi.RedmineManagerFactory;
import com.taskadapter.redmineapi.bean.Issue;
import com.taskadapter.redmineapi.bean.Project;
import com.taskadapter.redmineapi.bean.TimeEntry;
import com.taskadapter.redmineapi.bean.User;
import com.taskadapter.redmineapi.bean.Version;

import akka.actor.Cancellable;
import constants.MafDataType;
import dao.delivery.IterationDAO;
import dao.delivery.RequirementDAO;
import dao.pmo.ActorDao;
import dao.pmo.PortfolioEntryDao;
import framework.commons.message.EventMessage;
import framework.commons.message.EventMessage.MessageType;
import framework.services.plugins.api.IPluginContext;
import framework.services.plugins.api.IPluginContext.LogLevel;
import framework.services.plugins.api.IPluginRunner;
import framework.services.plugins.api.PluginException;
import framework.services.system.ISysAdminUtils;
import models.delivery.Iteration;
import models.delivery.Requirement;
import models.framework_models.common.CustomAttributeDefinition;
import models.framework_models.common.ICustomAttributeValue;
import models.pmo.Actor;
import models.pmo.PortfolioEntry;
import play.Logger;
import scala.concurrent.duration.Duration;
import scala.concurrent.duration.FiniteDuration;

/**
 * The abstract redmine plugin runner. It allows to manage the synchronisation
 * between redmine issues and portfolio entries.
 * 
 * @author Pierre-Yves Cloux
 * @author Johann Kohler
 */
public abstract class RedminePluginRunner implements IPluginRunner {

    private static final int REDMINE_API_ISSUES_LIMIT = 100;

    /**
     * List of actions.
     * 
     * @author Johann Kohler
     * 
     */
    public static enum ActionMessage {
        TRIGGER_PE_LOAD;
    }

    public static final String REQUIREMENT_STATUS_MAPPING_CONFIGURATION_NAME = "requirement_status_mapping";
    public static final String REQUIREMENT_PRIORITY_MAPPING_CONFIGURATION_NAME = "requirement_priority_mapping";
    public static final String REQUIREMENT_SEVERITY_MAPPING_CONFIGURATION_NAME = "requirement_severity_mapping";

    public static final String MAIN_CONFIGURATION_NAME = "config";
    public static final String REDMINE_HOST_URL_PROPERTY = "redmine.host.url";
    public static final String REDMINE_API_KEY_PROPERTY = "redmine.api.key";
    public static final String IS_SCOPED_CUSTOM_FIELD_ID = "is_scoped.custom_field.id";
    public static final String STORY_POINTS_CUSTOM_FIELD_ID = "story_points.custom_field.id";
    public static final String REMAINING_EFFORT_CUSTOM_FIELD_ID = "remaining_effort.custom_field.id";

    // properties for the portfolio entries
    public static final String PE_ITERATIONS_FILTER_PROPERTY = "pe.iterations.filter";
    public static final String PE_NEEDS_FILTER_PROPERTY = "pe.needs.filter";
    public static final String PE_NEEDS_TRACKERS_PROPERTY = "pe.needs.trackers";
    public static final String PE_DEFECTS_FILTER_PROPERTY = "pe.defects.filter";
    public static final String PE_DEFECTS_TRACKERS_PROPERTY = "pe.defects.trackers";
    public static final String PE_LOAD_START_TIME_PROPERTY = "pe.load.start.time";
    public static final String PE_LOAD_FREQUENCY_PROPERTY = "pe.load.frequency";

    protected IPluginContext pluginContext;

    protected String redmineHost;
    protected String apiAccessKey;

    protected Integer isScopedCustomFieldId;
    protected Integer storyPointsCustomFieldId;
    protected Integer remainingEffortFieldId;

    protected String peLoadStartTime;
    protected FiniteDuration peLoadFrequency;
    protected Cancellable currentScheduler;

    protected String[] peIterationsFilter;

    protected String[] peNeedsFilter;
    protected String[] peNeedsTrackers;

    protected String[] peDefectsFilter;
    protected String[] peDefectsTrackers;

    protected String peIssuesPatternUrl;

    protected Map<Integer, Long> requirementStatusMapping;
    protected Map<Integer, Long> requirementPriorityMapping;
    protected Map<Integer, Long> requirementSeverityMapping;

    public static final String PORTFOLIO_ENTRY_LINK_TYPE = "PORTFOLIO_ENTRY";
    public static final String PORTFOLIO_ENTRY_REQUIREMENT_LINK_TYPE = "PORTFOLIO_ENTRY_REQUIREMENT";
    public static final String PORTFOLIO_ENTRY_ITERATION_LINK_TYPE = "PORTFOLIO_ENTRY_ITERATION";

    protected static final int MINIMAL_FREQUENCY = 5;

    private ISysAdminUtils sysAdminUtils;

    public RedminePluginRunner(IPluginContext pluginContext, ISysAdminUtils sysAdminUtils) {
        this.pluginContext = pluginContext;
        this.sysAdminUtils = sysAdminUtils;
    }

    /**
     * Start the plugin.
     * 
     * @param properties
     *            the main properties
     */
    public void start(PropertiesConfiguration properties) throws PluginException {
        getPluginContext().log(LogLevel.DEBUG, "Redmine plugin checking properties");
        properties.setThrowExceptionOnMissing(true);
        this.apiAccessKey = properties.getString(REDMINE_API_KEY_PROPERTY);
        this.redmineHost = properties.getString(REDMINE_HOST_URL_PROPERTY);

        try {
            this.isScopedCustomFieldId = properties.getInt(IS_SCOPED_CUSTOM_FIELD_ID);
        } catch (Exception e) {
            this.isScopedCustomFieldId = null;
        }
        try {
            this.storyPointsCustomFieldId = properties.getInt(STORY_POINTS_CUSTOM_FIELD_ID);
        } catch (Exception e) {
            this.storyPointsCustomFieldId = null;
        }
        try {
            this.remainingEffortFieldId = properties.getInt(REMAINING_EFFORT_CUSTOM_FIELD_ID);
        } catch (Exception e) {
            this.remainingEffortFieldId = null;
        }

        this.peLoadStartTime = properties.getString(PE_LOAD_START_TIME_PROPERTY);
        if (this.peLoadStartTime == null || !this.peLoadStartTime.matches("^([01]?[0-9]|2[0-3])h[0-5][0-9]$")) {
            throw new IllegalArgumentException(
                    "Invalid time format for the " + PE_LOAD_START_TIME_PROPERTY + " parameter");
        }
        this.peLoadFrequency = FiniteDuration.create(properties.getLong(PE_LOAD_FREQUENCY_PROPERTY),
                TimeUnit.MINUTES);
        if (properties.getLong(PE_LOAD_FREQUENCY_PROPERTY) < MINIMAL_FREQUENCY) {
            throw new IllegalArgumentException(
                    "Invalid frequency " + PE_LOAD_FREQUENCY_PROPERTY + " must be more than 5 minutes");
        }

        this.peIterationsFilter = null;
        String peIterationsFilterString = properties.getString(PE_ITERATIONS_FILTER_PROPERTY);
        if (peIterationsFilterString != null && !peIterationsFilterString.equals("")) {
            String[] peIterationsFilter = peIterationsFilterString.split("=");
            if (peIterationsFilter.length == 2) {
                this.peIterationsFilter = peIterationsFilter;
            }
        }

        this.peNeedsFilter = null;
        String peNeedsFilterString = properties.getString(PE_NEEDS_FILTER_PROPERTY);
        if (peNeedsFilterString != null && !peNeedsFilterString.equals("")) {
            String[] peNeedsFilter = peNeedsFilterString.split("=");
            if (peNeedsFilter.length == 2) {
                this.peNeedsFilter = peNeedsFilter;
            }
        }

        this.peNeedsTrackers = null;
        String peNeedsTrackersString = properties.getString(PE_NEEDS_TRACKERS_PROPERTY);
        if (peNeedsTrackersString != null && !peNeedsTrackersString.equals("")) {
            this.peNeedsTrackers = peNeedsTrackersString.split(";");
        }

        this.peDefectsFilter = null;
        String peDefectsFilterString = properties.getString(PE_DEFECTS_FILTER_PROPERTY);
        if (peDefectsFilterString != null && !peDefectsFilterString.equals("")) {
            String[] peDefectsFilter = peDefectsFilterString.split("=");
            if (peDefectsFilter.length == 2) {
                this.peDefectsFilter = peDefectsFilter;
            }
        }

        this.peDefectsTrackers = null;
        String peDefectsTrackersString = properties.getString(PE_DEFECTS_TRACKERS_PROPERTY);
        if (peDefectsTrackersString != null && !peDefectsTrackersString.equals("")) {
            this.peDefectsTrackers = peDefectsTrackersString.split(";");
        }

        this.peIssuesPatternUrl = properties.getString(REDMINE_HOST_URL_PROPERTY) + "/issues/{externalId}";

        if (!isAvailable()) {
            throw new PluginException("Server not available : stopping");
        }

        getPluginContext().log(LogLevel.DEBUG, "Preparing the scheduler");
        try {
            long howMuchMinutesUntilStartTime = howMuchMinutesUntilStartTime();
            currentScheduler = getSysAdminUtils().scheduleRecurring(true,
                    "RedminePlugin PortfolioEntry load " + getPluginContext().getPluginConfigurationName(),
                    Duration.create(howMuchMinutesUntilStartTime, TimeUnit.MINUTES), this.peLoadFrequency,
                    new Runnable() {
                        @Override
                        public void run() {
                            runPortfolioEntryLoad();
                        }
                    });
            String startTimeMessage = String.format("Scheduler programmed to run in %d minutes",
                    howMuchMinutesUntilStartTime);
            getPluginContext().log(LogLevel.INFO, startTimeMessage);
        } catch (Exception e) {
            if (e instanceof PluginException) {
                throw (PluginException) e;
            }
            throw new PluginException(e);
        }

        // get the requirement status mapping
        this.requirementStatusMapping = new HashMap<Integer, Long>();
        PropertiesConfiguration statusMapping = getPluginContext().getPropertiesConfigurationFromByteArray(
                getPluginContext().getConfigurationAndMergeWithDefault(getPluginContext().getPluginDescriptor()
                        .getConfigurationBlockDescriptors().get(REQUIREMENT_STATUS_MAPPING_CONFIGURATION_NAME)));
        for (Iterator<String> iter = statusMapping.getKeys(); iter.hasNext();) {
            String key = iter.next();
            Integer redmineStatusId = Integer.parseInt(key);
            this.requirementStatusMapping.put(redmineStatusId, statusMapping.getLong(key));
        }

        // get the requirement priority mapping
        this.requirementPriorityMapping = new HashMap<Integer, Long>();
        PropertiesConfiguration priorityMapping = getPluginContext().getPropertiesConfigurationFromByteArray(
                getPluginContext().getConfigurationAndMergeWithDefault(getPluginContext().getPluginDescriptor()
                        .getConfigurationBlockDescriptors().get(REQUIREMENT_PRIORITY_MAPPING_CONFIGURATION_NAME)));
        for (Iterator<String> iter = priorityMapping.getKeys(); iter.hasNext();) {
            String key = iter.next();
            Integer redminePriorityId = Integer.parseInt(key);
            this.requirementPriorityMapping.put(redminePriorityId, priorityMapping.getLong(key));
        }

        // get the requirement severity mapping
        this.requirementSeverityMapping = new HashMap<Integer, Long>();
        PropertiesConfiguration severityMapping = getPluginContext().getPropertiesConfigurationFromByteArray(
                getPluginContext().getConfigurationAndMergeWithDefault(getPluginContext().getPluginDescriptor()
                        .getConfigurationBlockDescriptors().get(REQUIREMENT_SEVERITY_MAPPING_CONFIGURATION_NAME)));
        for (Iterator<String> iter = severityMapping.getKeys(); iter.hasNext();) {
            String key = iter.next();
            Integer redminePriorityId = Integer.parseInt(key);
            this.requirementSeverityMapping.put(redminePriorityId, severityMapping.getLong(key));
        }

        getPluginContext().log(LogLevel.INFO, "Redmine plugin started");

    }

    /**
     * Return true if the server is available.
     */
    private boolean isAvailable() {
        RedmineManager redmineManager = null;
        try {
            redmineManager = getRedmineManager();
            User user = redmineManager.getUserManager().getCurrentUser();
            return user != null;
        } catch (Exception e) {
            getPluginContext().log(LogLevel.ERROR, "The server seems not accessible", e);

            return false;
        } finally {
            if (redmineManager != null) {
                redmineManager.shutdown();
            }
        }
    }

    @Override
    public void stop() {
        try {
            if (this.currentScheduler != null) {
                this.currentScheduler.cancel();
                getPluginContext().log(LogLevel.INFO, "Scheduler stopped");
            }
        } catch (Exception e) {
            getPluginContext().log(LogLevel.ERROR, "Error when stopping the redmine plugin scheduler", e);
        }
        getPluginContext().log(LogLevel.INFO, "Redmine plugin stopped");
    }

    @Override
    public synchronized void handleOutProvisioningMessage(EventMessage eventMessage) throws PluginException {

        if (eventMessage.getMessageType().equals(MessageType.CUSTOM) && eventMessage.getPayload() != null
                && eventMessage.getPayload() instanceof ActionMessage) {
            switch ((ActionMessage) eventMessage.getPayload()) {
            case TRIGGER_PE_LOAD:
                runPortfolioEntryLoad(eventMessage.getInternalId());
                break;
            }

        }
    }

    /**
     * Run the load for all active portfolio entries.
     */
    private void runPortfolioEntryLoad() {
        try {
            List<PortfolioEntry> portfolioEntries = PortfolioEntryDao.getPEAsExpr(false).findList();
            for (PortfolioEntry portfolioEntry : portfolioEntries) {
                if (getPluginContext().isRegistered(MafDataType.getPortfolioEntry(), portfolioEntry.id)) {
                    runPortfolioEntryLoad(portfolioEntry.id);
                }
            }
        } catch (Exception e) {
            this.getPluginContext().log(LogLevel.ERROR,
                    "error when running runPortfolioEntryLoad from the scheduler", e);
        }
    }

    /**
     * Run the load for a portfolio entry.
     * 
     * @param portfolioEntryId
     *            the portfolio entry id
     */
    private void runPortfolioEntryLoad(Long portfolioEntryId) throws PluginException {

        if (getPluginContext().isRegistered(MafDataType.getPortfolioEntry(), portfolioEntryId)) {

            // get the registration configuration properties
            PropertiesConfiguration registrationProperties = getPluginContext()
                    .getPropertiesConfigurationFromByteArray(pluginContext
                            .getRegistrationConfiguration(MafDataType.getPortfolioEntry(), portfolioEntryId));

            // get the portfolio entry
            PortfolioEntry portfolioEntry = PortfolioEntryDao.getPEById(portfolioEntryId);

            // load the iterations
            if (getBooleanProperty(registrationProperties, "iterations")) {
                loadIterations(portfolioEntry);
            }

            // load the needs
            if (getBooleanProperty(registrationProperties, "needs")) {
                loadRequirements(portfolioEntry, false);
            }

            // load the defects
            if (getBooleanProperty(registrationProperties, "defects")) {
                loadRequirements(portfolioEntry, true);
            }

        } else {
            this.getPluginContext().log(LogLevel.ERROR,
                    "runPortfolioEntryLoad: the portfolio entry " + portfolioEntryId + " is not registered");
        }

    }

    /**
     * Load the iterations of a portfolio entry.
     * 
     * @param portfolioEntry
     *            the portfolio entry
     */
    private void loadIterations(PortfolioEntry portfolioEntry) {

        // load the filter
        Integer customFieldIdFilter = null;
        String peValueFilter = null;
        if (this.peIterationsFilter != null) {

            try {
                // extract the id of the custom field from the pattern cf_{id}
                String[] tmp = peIterationsFilter[0].split("_");
                customFieldIdFilter = Integer.valueOf(tmp[1]);
            } catch (Exception e) {
                Logger.warn("loadIterations: impossible to identify the redmine custom field for "
                        + peIterationsFilter[0]);
                EventMessage eventMessage = new EventMessage(portfolioEntry.id, MafDataType.getPortfolioEntry(),
                        MessageType.CUSTOM);
                this.getPluginContext().reportOnEventHandling("", true, eventMessage,
                        "An error has occured when loading the iterations: impossible to identify the Redmine custom field with id "
                                + peIterationsFilter[0]);
            }

            if (customFieldIdFilter != null) {

                try {

                    // try with direct PE attributes
                    Class<?> c = portfolioEntry.getClass();
                    Field f = c.getDeclaredField(peIterationsFilter[1]);
                    f.setAccessible(true);
                    if (f.get(portfolioEntry) != null) {
                        peValueFilter = f.get(portfolioEntry).toString().trim().toLowerCase();
                    } else {
                        peValueFilter = "";
                    }

                } catch (Exception e) {

                    // else, try with custom attributes of the PE
                    ICustomAttributeValue caValue = null;
                    try {
                        caValue = CustomAttributeDefinition.getCustomAttributeValue(peIterationsFilter[1],
                                PortfolioEntry.class, portfolioEntry.id);
                    } catch (Exception e1) {
                    }

                    if (caValue != null) {
                        if (caValue.getValueAsObject() != null) {
                            peValueFilter = caValue.getValueAsObject().toString().trim().toLowerCase();
                        } else {
                            peValueFilter = "";
                        }
                    } else {
                        Logger.warn(
                                "loadIterations: impossible to load the PE field or custom attribute for the filter "
                                        + peIterationsFilter[1]);
                        EventMessage eventMessage = new EventMessage(portfolioEntry.id,
                                MafDataType.getPortfolioEntry(), MessageType.CUSTOM);
                        this.getPluginContext().reportOnEventHandling("", true, eventMessage,
                                "An error has occured when loading the iterations: impossible to identify the BizDock attribute with id "
                                        + peIterationsFilter[1]);

                    }

                }

            }
        }

        // get the redmine manager
        RedmineManager redmineManager = getRedmineManager();

        try {

            for (String externalId : getPluginContext().getMultipleExternalId(portfolioEntry.id,
                    PORTFOLIO_ENTRY_LINK_TYPE)) {

                // initialize the iterations to remove
                Map<String, Long> iterationsToRemove = new HashMap<String, Long>();
                for (Pair<Long, String> child : pluginContext.getChildrenIdOfLink(portfolioEntry.id, externalId,
                        PORTFOLIO_ENTRY_LINK_TYPE, PORTFOLIO_ENTRY_ITERATION_LINK_TYPE)) {
                    iterationsToRemove.put(child.getRight(), child.getLeft());
                }

                Project project = redmineManager.getProjectManager().getProjectByKey(externalId);
                List<Version> versions = redmineManager.getProjectManager()
                        .getVersions(Integer.parseInt(externalId));

                for (Version version : versions) {

                    if (peValueFilter == null || customFieldIdFilter == null
                            || (version.getCustomFieldById(customFieldIdFilter) != null
                                    && ((version.getCustomFieldById(customFieldIdFilter).getValue() != null
                                            && version.getCustomFieldById(customFieldIdFilter).getValue().trim()
                                                    .toLowerCase().equals(peValueFilter))
                                            || (version.getCustomFieldById(customFieldIdFilter).getValue() == null
                                                    && peValueFilter.equals(""))))) {

                        String versionId = String.valueOf(version.getId());

                        iterationsToRemove.remove(versionId);

                        // try to get the iteration id
                        Long iterationId = pluginContext.getUniqueInternalIdWithParent(versionId,
                                PORTFOLIO_ENTRY_ITERATION_LINK_TYPE, portfolioEntry.id, externalId,
                                PORTFOLIO_ENTRY_LINK_TYPE);

                        Iteration iteration = null;
                        if (iterationId != null) {
                            iteration = IterationDAO.getIterationById(iterationId);
                        } else {
                            iteration = new Iteration();
                            iteration.portfolioEntry = portfolioEntry;
                            iteration.storyPoints = null;
                        }

                        iteration.description = version.getDescription();
                        iteration.endDate = version.getDueDate();
                        if (version.getStatus() != null && version.getStatus().equals("closed")) {
                            iteration.isClosed = true;
                        } else {
                            iteration.isClosed = false;
                        }
                        iteration.name = version.getName();
                        iteration.startDate = null;
                        iteration.source = "Redmine - " + project.getName();

                        iteration.save();

                        // save the link
                        if (iterationId == null) {
                            try {
                                getPluginContext().createLink(iteration.id, versionId,
                                        PORTFOLIO_ENTRY_ITERATION_LINK_TYPE, portfolioEntry.id, externalId,
                                        PORTFOLIO_ENTRY_LINK_TYPE);
                            } catch (PluginException e) {
                                this.getPluginContext().log(LogLevel.ERROR,
                                        "impossible to create the iteration link", e);
                                iteration.doDelete();
                            }
                        }

                    }

                }

                // remove the iterations
                for (Map.Entry<String, Long> entry : iterationsToRemove.entrySet()) {
                    Iteration iteration = IterationDAO.getIterationById(entry.getValue());
                    if (iteration != null) {
                        iteration.doDelete();
                        pluginContext.deleteLink(entry.getValue(), entry.getKey(),
                                PORTFOLIO_ENTRY_ITERATION_LINK_TYPE);
                    }
                }

            }
        } catch (RedmineException e) {
            Logger.warn("runPortfolioEntryLoad: impossible to get the versions for the portfolio entry "
                    + portfolioEntry.id, e);
            EventMessage eventMessage = new EventMessage(portfolioEntry.id, MafDataType.getPortfolioEntry(),
                    MessageType.CUSTOM);
            this.getPluginContext().reportOnEventHandling("", true, eventMessage,
                    "An error has occurred when loading the versions for the Initiative " + portfolioEntry.id
                            + ". The message returned by Redmine is: " + e.getMessage());
        }
    }

    /**
     * Load the requirements of a portfolio entry.
     * 
     * @param portfolioEntry
     *            the portfolio entry
     * @param isDefect
     *            set to true for the defects, set to false for the needs
     */
    private void loadRequirements(PortfolioEntry portfolioEntry, boolean isDefect) {

        String[] trackers = null;
        String[] filter = null;

        if (isDefect) {
            trackers = this.peDefectsTrackers;
            filter = this.peDefectsFilter;
        } else {
            trackers = this.peNeedsTrackers;
            filter = this.peNeedsFilter;
        }

        // get the redmine manager
        RedmineManager redmineManager = getRedmineManager();

        if (trackers != null) {

            Map<String, String> params = new HashMap<String, String>();
            params.put("status_id", "*");

            String categoryFilter = null;
            if (filter != null) {

                String peValueFilter = null;

                try {

                    // try with direct PE attributes
                    Class<?> c = portfolioEntry.getClass();
                    Field f = c.getDeclaredField(filter[1]);
                    f.setAccessible(true);

                    if (f.get(portfolioEntry) != null) {
                        peValueFilter = f.get(portfolioEntry).toString().trim();
                    } else {
                        peValueFilter = "";
                    }

                } catch (Exception e) {

                    // else, try with custom attributes of the PE
                    ICustomAttributeValue caValue = null;
                    try {
                        caValue = CustomAttributeDefinition.getCustomAttributeValue(filter[1], PortfolioEntry.class,
                                portfolioEntry.id);
                    } catch (Exception e1) {
                    }

                    if (caValue != null) {
                        if (caValue.getValueAsObject() != null) {
                            peValueFilter = caValue.getValueAsObject().toString().trim();
                        } else {
                            peValueFilter = "";
                        }

                    } else {
                        Logger.warn(
                                "loadRequirements: impossible to load the PE field or custom attribute for the filter "
                                        + filter[1]);
                        EventMessage eventMessage = new EventMessage(portfolioEntry.id,
                                MafDataType.getPortfolioEntry(), MessageType.CUSTOM);
                        this.getPluginContext().reportOnEventHandling("", true, eventMessage,
                                "An error has occured when loading the requirements: impossible to identify the BizDock attribute with id "
                                        + filter[1]);

                    }

                }

                if (peValueFilter != null) {
                    if (filter[0].equals("category")) {
                        categoryFilter = peValueFilter.toLowerCase();
                    } else {
                        params.put(filter[0], peValueFilter);
                    }
                }

            }

            // get all Redmine users and create a map userId <=> login
            Map<Integer, String> users = new HashMap<Integer, String>();
            try {
                for (User user : redmineManager.getUserManager().getUsers()) {
                    users.put(user.getId(), user.getLogin());
                }
            } catch (RedmineException e) {
                Logger.warn("loadRequirements: impossible to get the Redmine users", e);
                EventMessage eventMessage = new EventMessage(portfolioEntry.id, MafDataType.getPortfolioEntry(),
                        MessageType.CUSTOM);
                this.getPluginContext().reportOnEventHandling("", true, eventMessage,
                        "An error has occured when loading the requirements: impossible to get the Redmine users");

            }

            try {

                for (String externalId : getPluginContext().getMultipleExternalId(portfolioEntry.id,
                        PORTFOLIO_ENTRY_LINK_TYPE)) {

                    // initialize the requirements to remove
                    Map<String, Long> requirementsToRemove = new HashMap<String, Long>();
                    for (Pair<Long, String> child : pluginContext.getChildrenIdOfLink(portfolioEntry.id, externalId,
                            PORTFOLIO_ENTRY_LINK_TYPE, PORTFOLIO_ENTRY_REQUIREMENT_LINK_TYPE)) {
                        requirementsToRemove.put(child.getRight(), child.getLeft());
                    }

                    for (String trackerId : trackers) {

                        params.put("project_id", externalId);
                        params.put("tracker_id", trackerId);

                        List<Issue> issues = new ArrayList<>();
                        int loop = 0;
                        boolean stop = false;
                        while (!stop) {

                            String limit = String.valueOf(REDMINE_API_ISSUES_LIMIT);
                            String offset = String.valueOf(loop * REDMINE_API_ISSUES_LIMIT);

                            Logger.debug("loop: " + loop + ", offset: " + offset + ", limit:" + limit);

                            params.put("limit", limit);
                            params.put("offset", offset);

                            List<Issue> i = redmineManager.getIssueManager().getIssues(params);

                            if (i != null && i.size() > 0) {

                                Logger.debug("i: " + i);

                                issues.addAll(i);

                                if (i.size() < REDMINE_API_ISSUES_LIMIT) {
                                    Logger.debug("no more issue!");
                                    stop = true;
                                }
                            } else {
                                Logger.debug("no issue found!");
                                stop = true;
                            }

                            loop++;

                            /**
                             * Safety control: if there are more than 100*10000
                             * issues for one tracker and one issue, then this
                             * script is not compatible.
                             */
                            if (loop > 10000) {
                                stop = true;
                            }

                        }

                        for (Issue issue : issues) {

                            if (categoryFilter == null || (issue.getCategory() != null
                                    && categoryFilter.equals(issue.getCategory().getName().trim().toLowerCase()))) {

                                String issueId = String.valueOf(issue.getId());

                                requirementsToRemove.remove(issueId);

                                // try to get the requirement id
                                Long requirementId = pluginContext.getUniqueInternalIdWithParent(issueId,
                                        PORTFOLIO_ENTRY_REQUIREMENT_LINK_TYPE, portfolioEntry.id, externalId,
                                        PORTFOLIO_ENTRY_LINK_TYPE);

                                Requirement requirement = null;
                                if (requirementId != null) {
                                    requirement = RequirementDAO.getRequirementById(requirementId);
                                } else {
                                    requirement = new Requirement();
                                    requirement.isDefect = isDefect;
                                    requirement.portfolioEntry = portfolioEntry;
                                }

                                // get the author
                                Actor author = null;
                                if (users.containsKey(issue.getAuthor().getId())) {
                                    author = ActorDao.getActorByUid(users.get(issue.getAuthor().getId()));
                                }

                                requirement.externalRefId = issueId;
                                requirement.externalLink = this.peIssuesPatternUrl.replace("{externalId}",
                                        requirement.externalRefId);
                                requirement.name = issue.getSubject();
                                requirement.description = issue.getDescription();
                                requirement.category = issue.getCategory() != null ? issue.getCategory().getName()
                                        : null;
                                requirement.requirementStatus = this.requirementStatusMapping
                                        .containsKey(issue.getStatusId())
                                                ? RequirementDAO.getRequirementStatusById(
                                                        this.requirementStatusMapping.get(issue.getStatusId()))
                                                : null;
                                requirement.requirementPriority = this.requirementPriorityMapping
                                        .containsKey(issue.getPriorityId())
                                                ? RequirementDAO.getRequirementPriorityById(
                                                        this.requirementPriorityMapping.get(issue.getPriorityId()))
                                                : null;
                                requirement.requirementSeverity = this.requirementSeverityMapping
                                        .containsKey(issue.getPriorityId())
                                                ? RequirementDAO.getRequirementSeverityById(
                                                        this.requirementSeverityMapping.get(issue.getPriorityId()))
                                                : null;
                                requirement.author = author;

                                requirement.initialEstimation = issue.getEstimatedHours() != null
                                        ? new BigDecimal(issue.getEstimatedHours())
                                                .setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue()
                                        : null;

                                if (this.storyPointsCustomFieldId != null
                                        && issue.getCustomFieldById(storyPointsCustomFieldId) != null) {
                                    requirement.storyPoints = convertIntegerFromRedmineToJava(
                                            issue.getCustomFieldById(storyPointsCustomFieldId).getValue());
                                } else {
                                    requirement.storyPoints = null;
                                }

                                if (this.isScopedCustomFieldId != null
                                        && issue.getCustomFieldById(isScopedCustomFieldId) != null) {
                                    requirement.isScoped = convertBooleanFromRedmineToJava(
                                            issue.getCustomFieldById(isScopedCustomFieldId).getValue());
                                } else {
                                    requirement.isScoped = null;
                                }

                                if (this.remainingEffortFieldId != null
                                        && issue.getCustomFieldById(remainingEffortFieldId) != null) {
                                    requirement.remainingEffort = convertDoubleFromRedmineToJava(
                                            issue.getCustomFieldById(remainingEffortFieldId).getValue());
                                } else {
                                    requirement.remainingEffort = null;
                                }

                                List<TimeEntry> timeEntries = redmineManager.getIssueManager()
                                        .getTimeEntriesForIssue(issue.getId());
                                requirement.effort = 0d;
                                for (TimeEntry timeEntry : timeEntries) {
                                    requirement.effort += (double) timeEntry.getHours();
                                }

                                requirement.iteration = null;
                                if (issue.getTargetVersion() != null) {
                                    String versionId = String.valueOf(issue.getTargetVersion().getId());
                                    Long iterationId = pluginContext.getUniqueInternalIdWithParent(versionId,
                                            PORTFOLIO_ENTRY_ITERATION_LINK_TYPE, portfolioEntry.id, externalId,
                                            PORTFOLIO_ENTRY_LINK_TYPE);
                                    if (iterationId != null) {
                                        Iteration iteration = IterationDAO.getIterationById(iterationId);
                                        requirement.iteration = iteration;
                                    }
                                }

                                requirement.save();

                                // save the link
                                if (requirementId == null) {
                                    try {
                                        getPluginContext().createLink(requirement.id, issueId,
                                                PORTFOLIO_ENTRY_REQUIREMENT_LINK_TYPE, portfolioEntry.id,
                                                externalId, PORTFOLIO_ENTRY_LINK_TYPE);
                                    } catch (PluginException e) {
                                        this.getPluginContext().log(LogLevel.ERROR,
                                                "impossible to create the requirement link", e);
                                        requirement.doDelete();
                                    }
                                }

                            }

                        }

                    }

                    // remove the requirements
                    for (Map.Entry<String, Long> entry : requirementsToRemove.entrySet()) {
                        Requirement requirement = RequirementDAO.getRequirementById(entry.getValue());
                        if (requirement != null && requirement.isDefect == isDefect) {
                            requirement.doDelete();
                            pluginContext.deleteLink(entry.getValue(), entry.getKey(),
                                    PORTFOLIO_ENTRY_REQUIREMENT_LINK_TYPE);
                        }
                    }

                }
            } catch (RedmineException e) {
                Logger.warn("loadRequirements: impossible to get the requirements [isDefect=" + isDefect
                        + "] for the portfolio entry " + portfolioEntry.id, e);
                EventMessage eventMessage = new EventMessage(portfolioEntry.id, MafDataType.getPortfolioEntry(),
                        MessageType.CUSTOM);
                this.getPluginContext().reportOnEventHandling("", true, eventMessage,
                        "An error has occurred when loading the requirements for the Initiative "
                                + portfolioEntry.id + ". The message returned by Redmine is: " + e.getMessage());
            }

        } else {
            Logger.warn("loadRequirements: There is no tracker for the requirements [isDefect=" + isDefect + "]");
            EventMessage eventMessage = new EventMessage(portfolioEntry.id, MafDataType.getPortfolioEntry(),
                    MessageType.CUSTOM);
            this.getPluginContext().reportOnEventHandling("", true, eventMessage,
                    "An error has occured when loading the requirements: you need to configure trackers for needs and defects.");

        }

    }

    @Override
    public void handleInProvisioningMessage(EventMessage eventMessage) throws PluginException {
    }

    /**
     * Get the plugin context.
     */
    protected IPluginContext getPluginContext() {
        return pluginContext;
    }

    /**
     * Get the redmine host.
     */
    protected String getRedmineHost() {
        return redmineHost;
    }

    /**
     * Get the API access key.
     */
    private String getApiAccessKey() {
        return apiAccessKey;
    }

    /**
     * Return the object which can access the JSON endpoint of Redmine.
     * 
     * @return a manager
     */
    protected RedmineManager getRedmineManager() {
        return RedmineManagerFactory.createWithApiKey(getRedmineHost(), getApiAccessKey());
    }

    /**
     * Shut down (silently) the {@link RedmineManager}.
     * 
     * @param redmineManager
     *            the redmine manager
     */
    protected void shutDownRedmineManager(RedmineManager redmineManager) {
        try {
            if (redmineManager != null) {
                redmineManager.shutdown();
            }
        } catch (Exception e) {
            this.getPluginContext().log(LogLevel.ERROR, "Error when stopping the redmine manager", e);
        }
    }

    /**
     * Return the number of minutes until the next "start time".
     */
    private long howMuchMinutesUntilStartTime() {
        String time = this.peLoadStartTime;
        Date today = new Date();
        Calendar calendar = Calendar.getInstance();
        calendar.set(Calendar.HOUR_OF_DAY, Integer.valueOf(time.substring(0, 2)));
        calendar.set(Calendar.MINUTE, Integer.valueOf(time.substring(3, 5)));
        if (calendar.getTime().before(today)) {
            calendar.add(Calendar.DATE, 1);
        }
        long diff = calendar.getTime().getTime() - today.getTime();
        return diff / (60 * 1000);
    }

    /* HELPERS */

    /**
     * Get the properties for a portfolio entry registration as a byte array.
     * 
     * @param needs
     *            set to true if the needs should be synchronized
     * @param defects
     *            set to true if the defects should be synchronized
     * @param iterations
     *            set to true if the iterations should be synchronized
     */
    public static byte[] getPropertiesForPortfolioEntryAsByte(boolean needs, boolean defects, boolean iterations) {

        PropertiesConfiguration propertiesConfiguration = new PropertiesConfiguration();
        propertiesConfiguration.addProperty("needs", needs);
        propertiesConfiguration.addProperty("defects", defects);
        propertiesConfiguration.addProperty("iterations", iterations);

        Properties properties = ConfigurationConverter.getProperties(propertiesConfiguration);
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        try {
            properties.store(buffer, "properties");
            return buffer.toByteArray();
        } catch (IOException e) {
            Logger.error("impossible to store the properties in the buffer", e);
            return null;
        }

    }

    /**
     * Get the value of a boolean property from a list of properties.
     * 
     * If the property doesn't exist then return false.
     * 
     * @param propertiesConfiguration
     *            the available properties
     * @param propertyName
     *            the property name
     */
    public static boolean getBooleanProperty(PropertiesConfiguration propertiesConfiguration, String propertyName) {
        try {
            return propertiesConfiguration.getBoolean(propertyName);
        } catch (Exception e) {
            return false;
        }

    }

    /**
     * Convert a redmine Boolean (represented by a string) to a Java Boolean.
     * 
     * @param redmineBoolean
     *            the redmine boolean
     */
    private static Boolean convertBooleanFromRedmineToJava(String redmineBoolean) {
        if (redmineBoolean != null) {
            if (redmineBoolean.equals("1")) {
                return true;
            } else if (redmineBoolean.equals("0")) {
                return false;
            }
        }
        return null;
    }

    /**
     * Convert a redmine Integer (represented by a string) to a Java Integer.
     * 
     * @param redmineInteger
     *            the redmine integer
     * @return
     */
    private static Integer convertIntegerFromRedmineToJava(String redmineInteger) {
        try {
            return Integer.valueOf(redmineInteger);
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * Convert a redmine Double (represented by a string) to a Java Integer.
     * 
     * @param redmineDouble
     *            the redmine double
     * @return
     */
    private static Double convertDoubleFromRedmineToJava(String redmineDouble) {
        try {
            return Double.valueOf(redmineDouble);
        } catch (Exception e) {
            return null;
        }
    }

    private ISysAdminUtils getSysAdminUtils() {
        return sysAdminUtils;
    }
}