org.smartdeveloperhub.harvesters.it.testing.generator.ProjectActivityGenerator.java Source code

Java tutorial

Introduction

Here is the source code for org.smartdeveloperhub.harvesters.it.testing.generator.ProjectActivityGenerator.java

Source

/**
 * #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
 *   This file is part of the Smart Developer Hub Project:
 *     http://www.smartdeveloperhub.org/
 *
 *   Center for Open Middleware
 *     http://www.centeropenmiddleware.com/
 * #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
 *   Copyright (C) 2015-2016 Center for Open Middleware.
 * #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *             http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 * #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
 *   Artifact    : org.smartdeveloperhub.harvesters.it.frontend:it-frontend-dist:0.1.0
 *   Bundle      : it-frontend-dist-0.1.0.war
 * #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
 */
package org.smartdeveloperhub.harvesters.it.testing.generator;

import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;

import org.joda.time.DateTime;
import org.joda.time.Days;
import org.joda.time.Duration;
import org.joda.time.Hours;
import org.joda.time.LocalDate;
import org.joda.time.LocalDateTime;
import org.joda.time.LocalTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.smartdeveloperhub.harvesters.it.backend.ChangeLog;
import org.smartdeveloperhub.harvesters.it.backend.ChangeLog.Entry;
import org.smartdeveloperhub.harvesters.it.backend.ChangeLog.Entry.AssigneeChangeItem;
import org.smartdeveloperhub.harvesters.it.backend.ChangeLog.Entry.ClosedChangeItem;
import org.smartdeveloperhub.harvesters.it.backend.ChangeLog.Entry.ComponentsChangeItem;
import org.smartdeveloperhub.harvesters.it.backend.ChangeLog.Entry.DescriptionChangeItem;
import org.smartdeveloperhub.harvesters.it.backend.ChangeLog.Entry.DueToDateChangeItem;
import org.smartdeveloperhub.harvesters.it.backend.ChangeLog.Entry.EstimatedTimeChangeItem;
import org.smartdeveloperhub.harvesters.it.backend.ChangeLog.Entry.Item;
import org.smartdeveloperhub.harvesters.it.backend.ChangeLog.Entry.OpenedChangeItem;
import org.smartdeveloperhub.harvesters.it.backend.ChangeLog.Entry.PriorityChangeItem;
import org.smartdeveloperhub.harvesters.it.backend.ChangeLog.Entry.SeverityChangeItem;
import org.smartdeveloperhub.harvesters.it.backend.ChangeLog.Entry.StatusChangeItem;
import org.smartdeveloperhub.harvesters.it.backend.ChangeLog.Entry.TagsChangeItem;
import org.smartdeveloperhub.harvesters.it.backend.ChangeLog.Entry.TitleChangeItem;
import org.smartdeveloperhub.harvesters.it.backend.ChangeLog.Entry.TypeChangeItem;
import org.smartdeveloperhub.harvesters.it.backend.ChangeLog.Entry.VersionsChangeItem;
import org.smartdeveloperhub.harvesters.it.backend.Component;
import org.smartdeveloperhub.harvesters.it.backend.Contributor;
import org.smartdeveloperhub.harvesters.it.backend.Issue;
import org.smartdeveloperhub.harvesters.it.backend.Issue.Type;
import org.smartdeveloperhub.harvesters.it.backend.Priority;
import org.smartdeveloperhub.harvesters.it.backend.Project;
import org.smartdeveloperhub.harvesters.it.backend.Severity;
import org.smartdeveloperhub.harvesters.it.backend.Status;
import org.smartdeveloperhub.harvesters.it.backend.Version;
import org.smartdeveloperhub.harvesters.it.frontend.controller.LocalData;
import org.smartdeveloperhub.harvesters.it.frontend.testing.util.StateUtil;

import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.math.DoubleMath;

class ProjectActivityGenerator {

    private final class ComponentChangeInformationPoint implements ChangeInformationPoint {

        private final Issue issue;

        ComponentChangeInformationPoint(final Issue issue) {
            this.issue = issue;
        }

        @Override
        public boolean canModify() {
            return canAdd() || canRemove();
        }

        @Override
        public boolean canAdd() {
            return !ProjectActivityGenerator.this.components.isEmpty();
        }

        @Override
        public boolean canRemove() {
            return !this.issue.getComponents().isEmpty();
        }

    }

    private final class VersionsChangeInformationPoint implements ChangeInformationPoint {

        private final Issue issue;

        VersionsChangeInformationPoint(final Issue issue) {
            this.issue = issue;
        }

        @Override
        public boolean canModify() {
            return canAdd() || canRemove();
        }

        @Override
        public boolean canAdd() {
            return !ProjectActivityGenerator.this.versions.isEmpty();
        }

        @Override
        public boolean canRemove() {
            return !this.issue.getVersions().isEmpty();
        }

    }

    private final class TagsChangeInformationPoint implements ChangeInformationPoint {

        private final Issue issue;

        TagsChangeInformationPoint(final Issue issue) {
            this.issue = issue;
        }

        @Override
        public boolean canModify() {
            return true;
        }

        @Override
        public boolean canAdd() {
            return true;
        }

        @Override
        public boolean canRemove() {
            return !this.issue.getTags().isEmpty();
        }

    }

    private final class ComponentManager implements ChangeManager {

        private final Set<Item> changes;
        private final Set<String> issueComponents;
        private final Set<String> inFlight;
        private final List<String> added = Lists.newArrayList();
        private final List<String> removed = Lists.newArrayList();

        ComponentManager(final Issue issue, final Set<Item> changes) {
            this.changes = changes;
            this.issueComponents = issue.getComponents();
            this.inFlight = Sets.newLinkedHashSet();
        }

        private String selectExistingComponent() {
            final List<String> componentList = Lists.newArrayList(this.issueComponents);
            return componentList.get(ProjectActivityGenerator.this.random.nextInt(componentList.size()));
        }

        @Override
        public void remove() {
            final String componentId = selectExistingComponent();
            if (this.inFlight.contains(componentId)) {
                return;
            }
            final Component component = findComponent(componentId);
            if (this.issueComponents.remove(component.getId())) {
                this.inFlight.add(componentId);
                this.removed.add(component.getName());
                final ComponentsChangeItem item = new ComponentsChangeItem();
                item.setOldValue(componentId);
                item.setNewValue(null);
                this.changes.add(item);
            }
        }

        @Override
        public void add() {
            final Component component = selectComponent();
            if (this.inFlight.contains(component.getId())) {
                return;
            }
            if (this.issueComponents.add(component.getId())) {
                this.inFlight.add(component.getId());
                this.added.add(component.getName());
                final ComponentsChangeItem item = new ComponentsChangeItem();
                item.setOldValue(null);
                item.setNewValue(component.getId());
                this.changes.add(item);
            }
        }

        void logActivity() {
            if (!this.added.isEmpty()) {
                LOGGER.trace("     + Related to components: {}", Joiner.on(", ").join(this.added));
            }
            if (!this.removed.isEmpty()) {
                LOGGER.trace("     + Unrelated from components: {}", Joiner.on(", ").join(this.removed));
            }
        }
    }

    private final class VersionManager implements ChangeManager {

        private final Set<Item> changes;
        private final Set<String> issueVersions;
        private final Set<String> inFlight;
        private final List<String> added = Lists.newArrayList();
        private final List<String> removed = Lists.newArrayList();

        VersionManager(final Issue issue, final Set<Item> changes) {
            this.changes = changes;
            this.issueVersions = issue.getVersions();
            this.inFlight = Sets.newLinkedHashSet();
        }

        private String selectExistingVersion() {
            final List<String> versionList = Lists.newArrayList(this.issueVersions);
            return versionList.get(ProjectActivityGenerator.this.random.nextInt(versionList.size()));
        }

        @Override
        public void remove() {
            final String versionId = selectExistingVersion();
            if (this.inFlight.contains(versionId)) {
                return;
            }
            final Version version = findVersion(versionId);
            if (this.issueVersions.remove(version.getId())) {
                this.removed.add(version.getName());
                final VersionsChangeItem item = new VersionsChangeItem();
                item.setOldValue(versionId);
                item.setNewValue(null);
                this.changes.add(item);
            }
        }

        @Override
        public void add() {
            final Version version = selectVersion();
            if (this.inFlight.contains(version.getId())) {
                return;
            }
            if (this.issueVersions.add(version.getId())) {
                this.inFlight.add(version.getId());
                this.added.add(version.getName());
                final VersionsChangeItem item = new VersionsChangeItem();
                item.setOldValue(null);
                item.setNewValue(version.getId());
                this.changes.add(item);
            }
        }

        void logActivity() {
            if (!this.added.isEmpty()) {
                LOGGER.trace("     + Related to versions: {}", Joiner.on(", ").join(this.added));
            }
            if (!this.removed.isEmpty()) {
                LOGGER.trace("     + Unrelated from versions: {}", Joiner.on(", ").join(this.removed));
            }
        }
    }

    private final class TagManager implements ChangeManager {

        private final Set<Item> changes;
        private final Set<String> issueTags;
        private final Set<String> inFlight;
        private final List<String> added = Lists.newArrayList();
        private final List<String> removed = Lists.newArrayList();

        TagManager(final Issue issue, final Set<Item> changes) {
            this.changes = changes;
            this.issueTags = issue.getTags();
            this.inFlight = Sets.newLinkedHashSet();
        }

        private String selectExistingTag() {
            final List<String> tagList = Lists.newArrayList(this.issueTags);
            return tagList.get(ProjectActivityGenerator.this.random.nextInt(tagList.size()));
        }

        @Override
        public void remove() {
            final String tag = selectExistingTag();
            if (this.inFlight.contains(tag)) {
                return;
            }
            if (this.issueTags.remove(tag)) {
                this.inFlight.add(tag);
                this.removed.add(tag);
                final TagsChangeItem item = new TagsChangeItem();
                item.setOldValue(tag);
                item.setNewValue(null);
                this.changes.add(item);
            }
        }

        @Override
        public void add() {
            final String tag = selectTag();
            if (this.inFlight.contains(tag)) {
                return;
            }
            if (this.issueTags.add(tag)) {
                this.inFlight.add(tag);
                this.added.add(tag);
                final TagsChangeItem item = new TagsChangeItem();
                item.setOldValue(null);
                item.setNewValue(tag);
                this.changes.add(item);
            }
        }

        private String selectTag() {
            return TAGS[ProjectActivityGenerator.this.random.nextInt(TAGS.length)];
        }

        void logActivity() {
            if (!this.added.isEmpty()) {
                LOGGER.trace("     + Added tags: {}", Joiner.on(", ").join(this.added));
            }
            if (!this.removed.isEmpty()) {
                LOGGER.trace("     + Removed tags: {}", Joiner.on(", ").join(this.removed));
            }
        }
    }

    private static final String[] COMPONENT_NAMES = { "Frontend", "Backend", "Harvester", "Publisher",
            "Orchestrator", "Engine", "Reasoner", "Collector", "Crawler", "Database", "Store", "Repository",
            "Adapter", "Bridge", "Mediator", "Planner", "Scheduler", "Client", "Proxy", "CLI", "GUI", "Generator",
            "Installer", "Translator", "Compiler", "Transpiler" };
    /**
     * Taken from:
     * - http://programmers.stackexchange.com/questions/129714/how-to-manage-github-issues-for-priority-etc
     * - https://robinpowered.com/blog/best-practice-system-for-organizing-and-tagging-github-issues/
     */
    private static final String[] TAGS = { "feature", "idea", "support", "confirmed", "deferred", "fix-committed",
            "incomplete", "rejected", "resolved", "feedback-needed", "help-needed", "progress-25", "progress-50",
            "progress-75", "reviewed", "queued", "question", "discussion", "security", "production", "stating",
            "test", "chore", "legal", "watchlist", "invalid", "wontfix", "duplicate", "on-hold", };

    private static final Duration MINIMUM_EFFORT = Hours.FOUR.toStandardDuration();
    private static final Logger LOGGER = LoggerFactory.getLogger(ProjectActivityGenerator.class);

    private final ProjectConfiguration configuration;
    private final List<Contributor> participants;

    private final Random random;
    private Project project;
    private Set<String> componentNames;
    private Map<String, Component> components;
    private Map<String, Version> versions;
    private Map<String, Issue> issues;
    private WorkDay workDay;
    private SemVer version;

    ProjectActivityGenerator(final ProjectConfiguration configuration) {
        this.configuration = configuration;
        this.participants = configuration.contributors();
        this.random = new Random();
    }

    void addData(final LocalData data) {
        bootstrapProject();
        populateProject();
        data.getProjects().add(this.project);
        data.getProjectComponents().put(this.project.getId(), Lists.newArrayList(this.components.values()));
        data.getProjectVersions().put(this.project.getId(), Lists.newArrayList(this.versions.values()));
        data.getProjectIssues().put(this.project.getId(), Lists.newArrayList(this.issues.values()));
    }

    private void bootstrapProject() {
        this.project = new Project();
        this.project.setId(this.configuration.id());
        this.project.setName(this.configuration.name());

        this.componentNames = Sets.newLinkedHashSet();
        this.components = Maps.newLinkedHashMap();
        this.versions = Maps.newLinkedHashMap();
        this.issues = Maps.newLinkedHashMap();

        this.workDay = new WorkDay(this.random);

        LOGGER.info("Bootstrapping project {} ({})", this.project.getName(), this.project.getId());
        LOGGER.info("- Project started on {} ({} days of ongoing work)", this.configuration.startedOn(),
                this.configuration.duration().getStandardDays());
        LOGGER.info("- {}", this.workDay);

        final int initialComponents = 1 + this.random.nextInt(3);
        for (int i = 0; i < initialComponents; i++) {
            createComponent();
        }
        this.version = SemVer.create();
        if (this.random.nextBoolean()) {
            createVersion();
        }
    }

    private void populateProject() {
        if (this.participants.isEmpty()) {
            LOGGER.info("- Skipping project {} ({}) population: no developers available", this.project.getName(),
                    this.project.getId());
            return;
        }
        LOGGER.info("- Populating project {} ({}):", this.project.getName(), this.project.getId());
        for (int day = 0; day < this.configuration.duration().getStandardDays(); day++) {
            final LocalDate today = this.configuration.startedOn().plusDays(day);
            if (Utils.isWorkingDay(today) || this.random.nextInt(1000) % 25 == 0) {
                labour(today);
            }
        }
    }

    private void labour(final LocalDate today) {
        LOGGER.debug("- Labour on {}working day {} in project {} ({}):", Utils.isWorkingDay(today) ? "" : "non-",
                today, this.project.getName(), this.project.getId());
        createNewComponents();
        createNewVersions();
        createNewIssues(today);
        evaluateIssues(today);
        workOnIssues(today);
        reopenIssues(today);
    }

    private void createNewComponents() {
        if (this.random.nextInt(1000) % 5 == 0) {
            createComponent();
        }
    }

    private void createNewVersions() {
        if (this.random.nextInt(10000) % 5 == 0) {
            createVersion();
        }
    }

    private void createNewIssues(final LocalDate today) {
        final int inProgressIssues = findIssuesByStatus(Status.IN_PROGRESS).size();
        final int openIssues = findIssuesByStatus(Status.OPEN).size();

        final int base = this.participants.size() > 3 ? this.participants.size() : 3;

        int newIssues = 0;
        if (openIssues < inProgressIssues) {
            newIssues = this.random.nextInt(base) + this.random.nextInt(base * 3) / 4;
        } else if (openIssues * 2 < inProgressIssues) {
            newIssues = this.random.nextInt(base * 3) / 4;
        } else if (openIssues * 3 < inProgressIssues) {
            newIssues = this.random.nextInt(base * 2) / 3;
        } else {
            newIssues = this.random.nextBoolean() ? 1 : 0;
        }

        final Iterator<LocalTime> time = this.workDay.workingTimes();
        int start = this.issues.size();
        for (int i = 0; i < newIssues; i++) {
            createIssue(Integer.toString(++start), today.toLocalDateTime(time.next()));
        }
    }

    private void createIssue(final String issueId, final LocalDateTime creationDate) {
        final Issue issue = new Issue();
        issue.setProjectId(this.project.getId());
        issue.setId(issueId);
        issue.setName(StateUtil.generateSentence());
        issue.setDescription(StateUtil.generateSentences(1, 2 + this.random.nextInt(8)));
        issue.setStatus(Status.OPEN);
        issue.setCreationDate(creationDate.toDateTime());
        issue.setOpened(issue.getCreationDate());
        Contributor assignee = null;
        final Contributor reporter = selectContributor();
        issue.setReporter(reporter.getId());

        LOGGER.trace("   * Created issue {} at {}, reported by {}", issue.getId(), creationDate,
                reporter.getName());

        issue.setSeverity(selectSeverity());
        LOGGER.trace("     + Severity: {}", issue.getSeverity());

        issue.setPriority(selectPriority());
        LOGGER.trace("     + Priority: {}", issue.getPriority());

        if (this.random.nextBoolean()) {
            issue.setType(selectType());
            LOGGER.trace("     + Type: {}", issue.getType());
        }

        if (this.random.nextBoolean()) {
            assignee = selectContributor();
            issue.setAssignee(assignee.getId());
            LOGGER.trace("     + Assigned to {}", assignee.getName());
        }

        if (this.random.nextBoolean()) {
            final LocalDateTime dueTo = createDueTo(creationDate);
            issue.setDueTo(dueTo.toDateTime());
            LOGGER.trace("     + Scheduled for {}", dueTo);
            if (this.random.nextBoolean()) {
                issue.setEstimatedTime(estimateEffort(creationDate, dueTo));
                LOGGER.trace("     + Estimated {} hours ", issue.getEstimatedTime().getStandardHours());
            }
        }

        if (!this.components.isEmpty() && this.random.nextBoolean()) {
            final int affectedComponents = 1 + this.random.nextInt(2);
            final List<String> names = Lists.newArrayList();
            for (int i = 0; i < affectedComponents; i++) {
                final Component component = selectComponent();
                if (issue.getComponents().add(component.getId())) {
                    names.add(component.getName());
                }
            }
            LOGGER.trace("     + Related to components: {}", Joiner.on(", ").join(names));
        }

        if (!this.versions.isEmpty() && this.random.nextBoolean()) {
            final int affectedVersions = 1 + this.random.nextInt(2);
            final List<String> names = Lists.newArrayList();
            for (int i = 0; i < affectedVersions; i++) {
                final Version version = selectVersion();
                if (issue.getVersions().add(version.getId())) {
                    names.add(version.getName());
                }
            }
            LOGGER.trace("     + Affects versions: {}", Joiner.on(", ").join(names));
        }

        this.issues.put(issueId, issue);
        this.project.getIssues().add(issueId);
        this.project.getTopIssues().add(issueId);
    }

    private void evaluateIssues(final LocalDate today) {
        for (final Issue issue : findIssuesByStatus(Status.OPEN)) {
            if (!isInFlight(issue, today) && canEvaluate(issue)) {
                evaluate(issue, today);
            }
        }
    }

    private void workOnIssues(final LocalDate today) {
        for (final Issue issue : findIssuesByStatus(Status.IN_PROGRESS)) {
            if (!isInFlight(issue, today) && canWorkOn(issue)) {
                workOn(issue, today);
            }
        }
    }

    private void reopenIssues(final LocalDate today) {
        for (final Issue issue : findIssuesByStatus(Status.CLOSED)) {
            if (!isInFlight(issue, today) && canReopen(issue)) {
                reopen(issue, today);
            }
        }
    }

    private void evaluate(final Issue issue, final LocalDate today) {
        final Set<Item> changes = Sets.newLinkedHashSet();
        final LocalDateTime now = today.toLocalDateTime(this.workDay.workingTime());
        LOGGER.trace("   * Evaluated issue {} at {}", issue.getId(), now);

        if (issue.getAssignee() == null) {
            final Contributor assignee = selectContributor();
            issue.setAssignee(assignee.getId());

            final AssigneeChangeItem item = new AssigneeChangeItem();
            item.setOldValue(null);
            item.setNewValue(issue.getAssignee());
            changes.add(item);

            LOGGER.trace("     + Assigned to {}", assignee.getName());
        }

        if (issue.getType() == null) {
            issue.setType(selectType());

            final TypeChangeItem item = new TypeChangeItem();
            item.setOldValue(null);
            item.setNewValue(issue.getType());
            changes.add(item);

            LOGGER.trace("     + Type: {}", issue.getType());
        }

        if (this.random.nextInt(100) < 10) {
            issue.setStatus(Status.CLOSED);
            issue.setClosed(now.toDateTime());

            final ClosedChangeItem item = new ClosedChangeItem();
            item.setOldValue(null);
            item.setNewValue(issue.getClosed());
            changes.add(item);

            LOGGER.trace("     + Action: close");
        } else {
            issue.setStatus(Status.IN_PROGRESS);
            LOGGER.trace("     + Action: start work");
        }

        final StatusChangeItem item = new StatusChangeItem();
        item.setOldValue(Status.OPEN);
        item.setNewValue(issue.getStatus());
        changes.add(item);

        final Entry entry = new Entry();
        entry.setTimeStamp(now.toDateTime());
        entry.setAuthor(issue.getAssignee());
        entry.getItems().addAll(changes);

        final ChangeLog changeLog = new ChangeLog();
        changeLog.getEntries().add(entry);

        issue.setChanges(changeLog);
    }

    private void workOn(final Issue issue, final LocalDate today) {
        final Set<Item> changes = Sets.newLinkedHashSet();
        final LocalDateTime now = today.toLocalDateTime(this.workDay.workingTime());
        LOGGER.trace("   * Worked on issue {} at {}", issue.getId(), now);

        // Bounce assignment
        if (this.random.nextBoolean()) {
            final Contributor oldValue = findContributor(issue.getAssignee());
            final Contributor newValue = selectAlternativeContributor(issue.getAssignee());
            if (newValue != null) {
                issue.setAssignee(newValue.getId());

                final AssigneeChangeItem item = new AssigneeChangeItem();
                item.setOldValue(oldValue.getId());
                item.setNewValue(issue.getAssignee());
                changes.add(item);

                LOGGER.trace("     + Changed assignment from {} to {}", oldValue.getName(), newValue.getName());
            }
        }

        // Change type
        if (this.random.nextBoolean()) {
            final Type oldValue = issue.getType();
            final Type newValue = selectAlternativeType(oldValue);

            issue.setType(newValue);

            final TypeChangeItem item = new TypeChangeItem();
            item.setOldValue(oldValue);
            item.setNewValue(newValue);
            changes.add(item);

            LOGGER.trace("     + Changed type from {} to {}", oldValue, newValue);
        }

        // Change title
        if (this.random.nextBoolean()) {
            final String oldValue = issue.getName();
            final String newValue = StateUtil.generateSentence();

            issue.setName(newValue);

            final TitleChangeItem item = new TitleChangeItem();
            item.setOldValue(oldValue);
            item.setNewValue(newValue);
            changes.add(item);

            LOGGER.trace("     + Changed title from '{}' to '{}'", oldValue, newValue);
        }

        // Change description
        if (this.random.nextBoolean()) {
            final String oldValue = issue.getName();
            final String newValue = StateUtil.generateSentences(1, 2 + this.random.nextInt(8));

            issue.setDescription(newValue);

            final DescriptionChangeItem item = new DescriptionChangeItem();
            item.setOldValue(oldValue);
            item.setNewValue(newValue);
            changes.add(item);

            LOGGER.trace("     + Changed description from '{}' to '{}'", oldValue, newValue);
        }

        // Change severity
        if (this.random.nextBoolean()) {
            final Severity oldValue = issue.getSeverity();
            final Severity newValue = selectAlternativeSeverity(oldValue);

            issue.setSeverity(newValue);

            final SeverityChangeItem item = new SeverityChangeItem();
            item.setOldValue(oldValue);
            item.setNewValue(newValue);
            changes.add(item);

            LOGGER.trace("     + Changed severity from {} to {}", oldValue, newValue);
        }

        // Change priority
        if (this.random.nextBoolean()) {
            final Priority oldValue = issue.getPriority();
            final Priority newValue = selectAlternativePriority(oldValue);

            issue.setPriority(newValue);

            final PriorityChangeItem item = new PriorityChangeItem();
            item.setOldValue(oldValue);
            item.setNewValue(newValue);
            changes.add(item);

            LOGGER.trace("     + Changed priority from {} to {}", oldValue, newValue);
        }

        // Change due to
        boolean reescheduled = false;
        boolean isCloseable = true;
        if (this.random.nextBoolean()) {
            isCloseable = false;
            reescheduled = scheduleIssue(issue, changes, now);
        }

        // Change effort, if required or decided to.
        if (reescheduled || issue.getDueTo() != null && this.random.nextBoolean()) {
            isCloseable = false;
            estimateIssue(issue, changes, now);
        }

        final ChangeDecissionPoint cdp = new ChangeDecissionPoint(this.random);

        // Add/remove components
        final ChangeInformationPoint ccip = new ComponentChangeInformationPoint(issue);
        if (ccip.canModify() && this.random.nextBoolean()) {
            final ComponentManager pep = new ComponentManager(issue, changes);
            final int affectedComponents = 1 + this.random.nextInt(Math.max(2, issue.getComponents().size() - 1));
            for (int i = 0; i < affectedComponents; i++) {
                cdp.decide(ccip).apply(pep);
            }
            pep.logActivity();
        }

        // Add/remove versions
        final ChangeInformationPoint vcip = new VersionsChangeInformationPoint(issue);
        if (vcip.canModify() && this.random.nextBoolean()) {
            final VersionManager pep = new VersionManager(issue, changes);
            final int affectedVersions = 1 + this.random.nextInt(Math.max(2, issue.getVersions().size() - 1));
            for (int i = 0; i < affectedVersions; i++) {
                cdp.decide(vcip).apply(pep);
            }
            pep.logActivity();
        }

        /**
         * TODO: Add logic for adding commits
         */
        /**
         * TODO: Add logic for adding sub-issues
         */
        /**
         * TODO: Add logic for adding blocked-issues
         */

        // Add/remove tags
        final ChangeInformationPoint tcip = new TagsChangeInformationPoint(issue);
        if (tcip.canModify() && this.random.nextBoolean()) {
            final TagManager pep = new TagManager(issue, changes);
            final int affectedTags = 1 + this.random.nextInt(Math.max(2, issue.getTags().size() - 1));
            for (int i = 0; i < affectedTags; i++) {
                cdp.decide(tcip).apply(pep);
            }
            pep.logActivity();
        }

        if (isCloseable && mustCloseIssue(issue, now)) {
            issue.setStatus(Status.CLOSED);
            issue.setClosed(now.toDateTime());

            final StatusChangeItem sChange = new StatusChangeItem();
            sChange.setOldValue(Status.IN_PROGRESS);
            sChange.setNewValue(Status.CLOSED);
            changes.add(sChange);

            final ClosedChangeItem cdChange = new ClosedChangeItem();
            cdChange.setOldValue(null);
            cdChange.setNewValue(issue.getClosed());
            changes.add(cdChange);

            LOGGER.trace("     + Action: close");
        }

        final Entry entry = new Entry();
        entry.setTimeStamp(now.toDateTime());
        entry.setAuthor(issue.getAssignee());
        entry.getItems().addAll(changes);

        final ChangeLog changeLog = issue.getChanges();
        changeLog.getEntries().add(entry);
    }

    private void estimateIssue(final Issue issue, final Set<Item> changes, final LocalDateTime now) {
        final LocalDateTime dueTo = Utils.toLocalDateTime(issue.getDueTo());
        final Duration oldValue = issue.getEstimatedTime();
        Duration newValue = null;
        do {
            newValue = estimateEffort(now, dueTo);
            if (MINIMUM_EFFORT.isEqual(newValue) && MINIMUM_EFFORT.isEqual(oldValue)) {
                return;
            }
        } while (newValue.isEqual(oldValue));

        issue.setEstimatedTime(newValue);

        final EstimatedTimeChangeItem item = new EstimatedTimeChangeItem();
        item.setOldValue(oldValue);
        item.setNewValue(newValue);
        changes.add(item);

        if (oldValue == null) {
            LOGGER.trace("     + Estimated {} hours", newValue.getStandardHours());
        } else {
            LOGGER.trace("     + Reestimated from {} to {} hours", oldValue.getStandardHours(),
                    newValue.getStandardHours());
        }
    }

    private boolean scheduleIssue(final Issue issue, final Set<Item> changes, final LocalDateTime now) {
        final LocalDateTime oldValue = Utils.toLocalDateTime(issue.getDueTo());
        final LocalDateTime newValue = createDueTo(now);

        issue.setDueTo(newValue.toDateTime());

        final DueToDateChangeItem item = new DueToDateChangeItem();
        item.setOldValue(Utils.toDateTime(oldValue));
        item.setNewValue(newValue.toDateTime());
        changes.add(item);

        boolean result = false;
        if (oldValue == null) {
            LOGGER.trace("     + Scheduled for {}", newValue);
        } else {
            result = true;
            LOGGER.trace("     + Reescheduled from {} to {}", oldValue, newValue);
        }
        return result;
    }

    private void reopen(final Issue issue, final LocalDate today) {
        final Set<Item> changes = Sets.newLinkedHashSet();
        final LocalDateTime now = today.toLocalDateTime(this.workDay.workingTime());
        LOGGER.trace("   * Reopened issue {} at {}", issue.getId(), now);

        // Bounce assignment
        if (this.random.nextBoolean()) {
            final Contributor oldValue = findContributor(issue.getAssignee());
            final Contributor newValue = selectContributor();

            issue.setAssignee(newValue.getId());

            final AssigneeChangeItem item = new AssigneeChangeItem();
            item.setOldValue(oldValue.getId());
            item.setNewValue(issue.getAssignee());
            changes.add(item);

            LOGGER.trace("     + Changed assignment from {} to {}", oldValue.getName(), newValue.getName());
        }

        {
            issue.setStatus(Status.OPEN);

            final StatusChangeItem item = new StatusChangeItem();
            item.setOldValue(Status.CLOSED);
            item.setNewValue(Status.OPEN);
            changes.add(item);
        }
        {
            final DateTime oldValue = issue.getOpened();
            issue.setOpened(now.toDateTime());

            final OpenedChangeItem item = new OpenedChangeItem();
            item.setOldValue(oldValue);
            item.setNewValue(issue.getOpened());
            changes.add(item);
        }
        {
            final DateTime oldValue = issue.getClosed();
            issue.setClosed(null);

            final ClosedChangeItem item = new ClosedChangeItem();
            item.setOldValue(oldValue);
            item.setNewValue(issue.getClosed());
            changes.add(item);
        }
        {
            boolean scheduled = false;
            if (this.random.nextBoolean()) {
                scheduled = true;
                scheduleIssue(issue, changes, now);
            } else {
                final DateTime oldValue = issue.getDueTo();
                if (oldValue != null) {
                    final DueToDateChangeItem item = new DueToDateChangeItem();
                    item.setOldValue(oldValue);
                    item.setNewValue(null);
                    changes.add(item);
                }
            }

            if (scheduled && this.random.nextBoolean()) {
                estimateIssue(issue, changes, now);
            } else {
                final Duration oldValue = issue.getEstimatedTime();
                if (oldValue != null) {
                    final EstimatedTimeChangeItem item = new EstimatedTimeChangeItem();
                    item.setOldValue(oldValue);
                    item.setNewValue(null);
                    changes.add(item);
                }
            }
        }

        final Entry entry = new Entry();
        entry.setTimeStamp(now.toDateTime());
        entry.setAuthor(issue.getAssignee());
        entry.getItems().addAll(changes);

        final ChangeLog changeLog = issue.getChanges();
        changeLog.getEntries().add(entry);
    }

    private boolean isInFlight(final Issue issue, final LocalDate today) {
        if (issue.getCreationDate().toLocalDate().equals(today)) {
            return true;
        }
        if (issue.getChanges() == null) {
            return false;
        }
        final Entry changeSet = Iterables.getLast(issue.getChanges().getEntries());
        if (changeSet == null) {
            return false;
        }
        final LocalDate lastChangeDate = changeSet.getTimeStamp().toLocalDate();
        return lastChangeDate.equals(today);
    }

    private boolean canEvaluate(final Issue issue) {
        return this.random.nextInt(1000) % 25 == 0;
    }

    private boolean canWorkOn(final Issue issue) {
        return this.random.nextInt(1000) % 50 == 0;
    }

    private boolean canReopen(final Issue issue) {
        return this.random.nextInt(1000) % 100 == 0;
    }

    private Duration estimateEffort(final LocalDateTime start, final LocalDateTime dueTo) {
        final Days daysBetween = Days.daysBetween(start, dueTo);
        int workingDays = 0;
        for (int i = 0; i < daysBetween.getDays(); i++) {
            if (Utils.isWorkingDay(start.toLocalDate().plusDays(i))) {
                workingDays++;
            }
        }
        final int maxMinutes = workingDays * this.workDay.effortPerDay();
        final double ratio = (100 + this.random.nextInt(900)) / 1000d;
        Duration result = Duration.standardMinutes(
                33 * maxMinutes / 100 + DoubleMath.roundToInt(67 * maxMinutes / 100 * ratio, RoundingMode.CEILING));
        if (result.isShorterThan(MINIMUM_EFFORT)) {
            result = MINIMUM_EFFORT;
        }
        return result;
    }

    private LocalDateTime createDueTo(final LocalDateTime dateTime) {
        LocalDate localDate = dateTime.toLocalDate().plusDays(1 + this.random.nextInt(15));
        while (Utils.isWorkingDay(localDate)) {
            localDate = localDate.plusDays(1);
        }
        return localDate.toLocalDateTime(this.workDay.workingHour());
    }

    private boolean mustCloseIssue(final Issue issue, final LocalDateTime now) {
        long threshold = 80;
        final DateTime dueTo = issue.getDueTo();
        if (dueTo != null) {
            final double mark = toPOSIXMillis(now);
            final double opened = toPOSIXMillis(issue.getOpened().toLocalDateTime());
            final double deadline = toPOSIXMillis(dueTo.toLocalDateTime());
            final double maxDeadline = toPOSIXMillis(dueTo.toLocalDateTime().plusDays(14));
            final long onTime = DoubleMath.roundToLong(90 * ((mark - opened) / (deadline - opened)),
                    RoundingMode.CEILING);
            final long delayed = DoubleMath.roundToLong(
                    10 * (Math.max(0, mark - deadline) / (maxDeadline - deadline)), RoundingMode.CEILING);
            threshold = onTime + delayed;
        }
        return this.random.nextInt(100) < threshold;
    }

    private double toPOSIXMillis(final LocalDateTime value) {
        return value.toDate().getTime();
    }

    private Type selectAlternativeType(final Type value) {
        Type alternativeValue = null;
        do {
            alternativeValue = selectType();
        } while (alternativeValue.equals(value));
        return alternativeValue;
    }

    private Severity selectAlternativeSeverity(final Severity value) {
        Severity alternativeValue = null;
        do {
            alternativeValue = selectSeverity();
        } while (alternativeValue.equals(value));
        return alternativeValue;
    }

    private Priority selectAlternativePriority(final Priority value) {
        Priority alternativeValue = null;
        do {
            alternativeValue = selectPriority();
        } while (alternativeValue.equals(value));
        return alternativeValue;
    }

    private Contributor selectAlternativeContributor(final String id) {
        Contributor alternativeValue = null;
        if (this.participants.size() != 1) {
            do {
                alternativeValue = selectContributor();
            } while (id.equals(alternativeValue.getId()));
        }
        return alternativeValue;
    }

    private Type selectType() {
        final int typeCase = this.random.nextInt(100) % 3;
        Type type = null;
        if (typeCase == 0) {
            type = Type.TASK;
        } else if (typeCase == 1) {
            type = Type.BUG;
        } else {
            type = Type.IMPROVEMENT;
        }
        return type;
    }

    private Severity selectSeverity() {
        final List<Severity> values = Lists.newArrayList(Severity.values());
        return values.get(this.random.nextInt(values.size()));
    }

    private Priority selectPriority() {
        final List<Priority> values = Lists.newArrayList(Priority.values());
        return values.get(this.random.nextInt(values.size()));
    }

    private Contributor selectContributor() {
        return this.participants.get(this.random.nextInt(this.participants.size() * 4) % this.participants.size());
    }

    private Component selectComponent() {
        final List<Component> currentComponents = Lists.newArrayList(this.components.values());
        return currentComponents.get(this.random.nextInt(currentComponents.size() * 4) % currentComponents.size());
    }

    private Version selectVersion() {
        final List<Version> currentVersions = Lists.newArrayList(this.versions.values());
        return currentVersions.get(this.random.nextInt(currentVersions.size() * 4) % currentVersions.size());
    }

    private Component findComponent(final String componentId) {
        final Component component = this.components.get(componentId);
        if (component == null) {
            throw new IllegalStateException("Unknown component '" + componentId + "'");
        }
        return component;
    }

    private Version findVersion(final String versionId) {
        final Version version = this.versions.get(versionId);
        if (version == null) {
            throw new IllegalStateException("Unknown version '" + versionId + "'");
        }
        return version;
    }

    private Contributor findContributor(final String contributorId) {
        for (final Contributor target : this.participants) {
            if (contributorId.equals(target.getId())) {
                return target;
            }
        }
        throw new IllegalStateException("Unknown contributor '" + contributorId + "'");
    }

    private ArrayList<Issue> findIssuesByStatus(final Status status) {
        return Lists.newArrayList(Iterables.filter(this.issues.values(), new Predicate<Issue>() {
            @Override
            public boolean apply(final Issue input) {
                return status.equals(input.getStatus());
            }
        }));
    }

    private Version createVersion() {
        final int versionUpdate = this.random.nextInt(1000);
        if (versionUpdate % 100 == 0) {
            this.version = this.version.nextMajor();
        } else if (versionUpdate % 25 == 0) {
            this.version = this.version.nextMinor();
        } else {
            this.version = this.version.nextMinor();
        }

        final Version version = new Version();
        version.setId(Integer.toString(this.versions.size() + 1));
        version.setName(this.version.toString());
        version.setProjectId(this.project.getId());
        this.versions.put(version.getId(), version);
        this.project.getVersions().add(version.getId());
        LOGGER.trace("- Created version {} ({}) for project {} ({}) ", version.getName(), version.getId(),
                this.project.getName(), this.project.getId());
        return version;
    }

    private Component createComponent() {
        if (this.componentNames.size() >= COMPONENT_NAMES.length) {
            LOGGER.trace(
                    "- Skipped component creation for project {} ({}): all predefined components have been created",
                    this.project.getName(), this.project.getId());
            return Iterables.getLast(this.components.values());
        }
        final Component component = new Component();
        component.setId(Integer.toString(this.components.size() + 1));
        int index = this.random.nextInt(COMPONENT_NAMES.length);
        while (this.componentNames.contains(COMPONENT_NAMES[index])) {
            index = (index + 1) % COMPONENT_NAMES.length;
        }
        component.setName(COMPONENT_NAMES[index]);
        component.setProjectId(this.project.getId());
        this.componentNames.add(component.getName());
        this.components.put(component.getId(), component);
        this.project.getComponents().add(component.getId());
        LOGGER.trace("- Created component {} ({}) for project {} ({}) ", component.getName(), component.getId(),
                this.project.getName(), this.project.getId());
        return component;
    }

}