package com.taskadapter.redmineapi.bean; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.Set; /** * Redmine's Issue. * <p> * Note that methods returning lists of elements (like getRelations(), getWatchers(), etc return * unmodifiable collections. * You need to use methods like addRelations() if you want to add elements, e.g.: * <pre> * issue.addRelations(Collections.singletonList(relation)); * </pre> * * @see <a href="http://www.redmine.org/projects/redmine/wiki/Rest_Issues">http://www.redmine.org/projects/redmine/wiki/Rest_Issues</a> */ public class Issue implements Identifiable { private final PropertyStorage storage; public final static Property<Integer> DATABASE_ID = new Property<>(Integer.class, "id"); public final static Property<String> SUBJECT = new Property<>(String.class, "subject"); public final static Property<Date> START_DATE = new Property<>(Date.class, "startDate"); public final static Property<Date> DUE_DATE = new Property<>(Date.class, "dueDate"); public final static Property<Date> CREATED_ON = new Property<>(Date.class, "createdOn"); public final static Property<Date> UPDATED_ON = new Property<>(Date.class, "updatedOn"); public final static Property<Integer> DONE_RATIO = new Property<>(Integer.class, "doneRatio"); public final static Property<Integer> PARENT_ID = new Property<>(Integer.class, "parentId"); public final static Property<Integer> PRIORITY_ID = new Property<>(Integer.class, "priorityId"); public final static Property<Float> ESTIMATED_HOURS = new Property<>(Float.class, "estimatedHours"); public final static Property<Float> SPENT_HOURS = new Property<>(Float.class, "spentHours"); public final static Property<Integer> ASSIGNEE_ID = new Property<>(Integer.class, "assigneeId"); public final static Property<String> ASSIGNEE_NAME = new Property<>(String.class, "assigneeName"); /** * Some comment describing an issue update. */ public final static Property<String> NOTES = new Property<String>(String.class, "notes"); public final static Property<Boolean> PRIVATE_NOTES = new Property<>(Boolean.class, "notes"); public final static Property<String> PRIORITY_TEXT = new Property<>(String.class, "priorityText"); public final static Property<Integer> PROJECT_ID = new Property<>(Integer.class, "projectId"); public final static Property<String> PROJECT_NAME = new Property<>(String.class, "projectName"); public final static Property<Integer> AUTHOR_ID = new Property<>(Integer.class, "authorId"); public final static Property<String> AUTHOR_NAME = new Property<>(String.class, "authorName"); public final static Property<Tracker> TRACKER = new Property<>(Tracker.class, "tracker"); public final static Property<String> DESCRIPTION = new Property<>(String.class, "description"); public final static Property<Date> CLOSED_ON = new Property<>(Date.class, "closedOn"); public final static Property<Integer> STATUS_ID = new Property<>(Integer.class, "statusId"); public final static Property<String> STATUS_NAME = new Property<>(String.class, "statusName"); public final static Property<Version> TARGET_VERSION = new Property<>(Version.class, "targetVersion"); public final static Property<IssueCategory> ISSUE_CATEGORY = new Property<>(IssueCategory.class, "issueCategory"); public final static Property<Boolean> PRIVATE_ISSUE = new Property<>(Boolean.class, "privateIssue"); /** * can't have two custom fields with the same ID in the collection, that's why it is declared * as a Set, not a List. */ public final static Property<Set<CustomField>> CUSTOM_FIELDS = (Property<Set<CustomField>>) new Property(Set.class, "customFields"); public final static Property<Set<Journal>> JOURNALS = (Property<Set<Journal>>) new Property(Set.class, "journals"); public final static Property<Set<IssueRelation>> RELATIONS = (Property<Set<IssueRelation>>) new Property(Set.class, "relations"); public final static Property<Set<Attachment>> ATTACHMENTS = (Property<Set<Attachment>>) new Property(Set.class, "attachments"); public final static Property<Set<Changeset>> CHANGESETS = (Property<Set<Changeset>>) new Property(Set.class, "changesets"); public final static Property<Set<Watcher>> WATCHERS = (Property<Set<Watcher>>) new Property(Set.class, "watchers"); public final static Property<Set<Issue>> CHILDREN = (Property<Set<Issue>>) new Property(Set.class, "children"); /** * @param id database ID. */ Issue(Integer id) { this(); storage.set(DATABASE_ID, id); } public Issue() { this.storage = new PropertyStorage(); initCollections(storage); } private void initCollections(PropertyStorage storage) { storage.set(CUSTOM_FIELDS, new HashSet<>()); storage.set(CHILDREN, new HashSet<>()); storage.set(WATCHERS, new HashSet<>()); storage.set(CHANGESETS, new HashSet<>()); storage.set(ATTACHMENTS, new HashSet<>()); storage.set(RELATIONS, new HashSet<>()); storage.set(JOURNALS, new HashSet<>()); } public Integer getProjectId() { return storage.get(PROJECT_ID); } public void setProjectId(Integer projectId) { storage.set(PROJECT_ID, projectId); } public String getProjectName() { return storage.get(PROJECT_NAME); } public void setProjectName(String name) { storage.set(PROJECT_NAME, name); } public Integer getDoneRatio() { return storage.get(DONE_RATIO); } public void setDoneRatio(Integer doneRatio) { storage.set(DONE_RATIO, doneRatio); } public String getPriorityText() { return storage.get(PRIORITY_TEXT); } /** * @deprecated This method has no effect when creating issues on Redmine Server, so we might as well just delete it * in the future releases. */ public void setPriorityText(String priority) { storage.set(PRIORITY_TEXT, priority); } /** * Redmine can be configured to allow group assignments for issues: * Configuration option: Settings -> Issue Tracking -> Allow issue assignment to groups * * <p>An assignee can be a user or a group</p> */ public Integer getAssigneeId() { return storage.get(ASSIGNEE_ID); } public void setAssigneeId(Integer assigneeId) { storage.set(ASSIGNEE_ID, assigneeId); } public String getAssigneeName() { return storage.get(ASSIGNEE_NAME); } public void setAssigneeName(String assigneeName) { storage.set(ASSIGNEE_NAME, assigneeName); } public Float getEstimatedHours() { return storage.get(ESTIMATED_HOURS); } public void setEstimatedHours(Float estimatedTime) { storage.set(ESTIMATED_HOURS, estimatedTime); } public Float getSpentHours() { return storage.get(SPENT_HOURS); } public void setSpentHours(Float spentHours) { storage.set(SPENT_HOURS, spentHours); } /** * Parent Issue ID, or NULL for issues without a parent. * * @return NULL, if there's no parent */ public Integer getParentId() { return storage.get(PARENT_ID); } public void setParentId(Integer parentId) { storage.set(PARENT_ID, parentId); } /** * @return database id for this object. can be NULL for Issues not added to Redmine yet */ @Override public Integer getId() { return storage.get(DATABASE_ID); } public String getSubject() { return storage.get(SUBJECT); } public void setSubject(String subject) { storage.set(SUBJECT, subject); } public Date getStartDate() { return storage.get(START_DATE); } public void setStartDate(Date startDate) { storage.set(START_DATE, startDate); } public Date getDueDate() { return storage.get(DUE_DATE); } public void setDueDate(Date dueDate) { storage.set(DUE_DATE, dueDate); } public Integer getAuthorId() { return storage.get(AUTHOR_ID); } public void setAuthorId(Integer id) { storage.set(AUTHOR_ID, id); } public String getAuthorName() { return storage.get(AUTHOR_NAME); } public void setAuthorName(String name) { storage.set(AUTHOR_NAME, name); } public Tracker getTracker() { return storage.get(TRACKER); } public void setTracker(Tracker tracker) { storage.set(TRACKER, tracker); } public String getDescription() { return storage.get(DESCRIPTION); } public void setDescription(String description) { storage.set(DESCRIPTION, description); } public Date getCreatedOn() { return storage.get(CREATED_ON); } public void setCreatedOn(Date createdOn) { storage.set(CREATED_ON, createdOn); } public Date getUpdatedOn() { return storage.get(UPDATED_ON); } public void setUpdatedOn(Date updatedOn) { storage.set(UPDATED_ON, updatedOn); } public Date getClosedOn() { return storage.get(CLOSED_ON); } public void setClosedOn(Date closedOn) { storage.set(CLOSED_ON, closedOn); } public Integer getStatusId() { return storage.get(STATUS_ID); } public void setStatusId(Integer statusId) { storage.set(STATUS_ID, statusId); } public String getStatusName() { return storage.get(STATUS_NAME); } public void setStatusName(String statusName) { storage.set(STATUS_NAME, statusName); } /** * @return unmodifiable collection of Custom Field objects. the collection may be empty, but it is never NULL. */ public Collection<CustomField> getCustomFields() { return Collections.unmodifiableCollection(storage.get(CUSTOM_FIELDS)); } public void clearCustomFields() { storage.set(CUSTOM_FIELDS, new HashSet<>()); } /** * NOTE: The custom field(s) <strong>must have correct database ID set</strong> to be saved to Redmine. This is Redmine REST API's limitation. */ public void addCustomFields(Collection<CustomField> customFields) { storage.get(CUSTOM_FIELDS).addAll(customFields); } /** * If there is a custom field with the same ID already present in the Issue, * the new field replaces the old one. * * @param customField the field to add to the issue. */ public void addCustomField(CustomField customField) { storage.get(CUSTOM_FIELDS).add(customField); } @Deprecated /** * This method should not be used by clients. "notes" only makes sense when creating/updating an issue - that is the * string value added along with the update. * <p> * use {@link #getJournals()} if you want to access previously saved notes. feel free to submit an enhancement * request to Redmine developers if you think this "notes - journals" separation looks weird... */ public String getNotes() { return storage.get(NOTES); } /** * @param notes Some comment describing the issue update */ public void setNotes(String notes) { storage.set(NOTES, notes); } public boolean isPrivateNotes() { return storage.get(PRIVATE_NOTES); } /** * @param privateNotes mark note as private */ public void setPrivateNotes(boolean privateNotes) { storage.set(PRIVATE_NOTES, privateNotes); } /** * Don't forget to use Include.journals flag when loading issue from Redmine server: * <pre> * Issue issue = issueManager.getIssueById(3205, Include.journals); * </pre> * @return unmodifiable collection of Journal entries or empty collection if no objects found. Never NULL. * @see com.taskadapter.redmineapi.Include#journals */ public Collection<Journal> getJournals() { return Collections.unmodifiableCollection(storage.get(JOURNALS)); } /** * Issue journals are created automatically when you update existing issues. * journal entries are essentially log records for changes you make. * you cannot just add log records without making actual changes. * this API method is misleading and it should only be used internally by Redmine Json parser * when parsing response from server. we should hide it from public. * * TODO hide this method. https://github.com/taskadapter/redmine-java-api/issues/199 */ public void addJournals(Collection<Journal> journals) { storage.get(JOURNALS).addAll(journals); } /** * Don't forget to use Include.changesets flag when loading issue from Redmine server: * <pre> * Issue issue = issueManager.getIssueById(3205, Include.changesets); * </pre> * @return unmodifiable collection of entries or empty collection if no objects found. * @see com.taskadapter.redmineapi.Include#changesets */ public Collection<Changeset> getChangesets() { return Collections.unmodifiableCollection(storage.get(CHANGESETS)); } public void addChangesets(Collection<Changeset> changesets) { storage.get(CHANGESETS).addAll(changesets); } /** * Don't forget to use Include.watchers flag when loading issue from Redmine server: * <pre> * Issue issue = issueManager.getIssueById(3205, Include.watchers); * </pre> * @return unmodifiable collection of entries or empty collection if no objects found. * @see com.taskadapter.redmineapi.Include#watchers */ public Collection<Watcher> getWatchers() { return Collections.unmodifiableCollection(storage.get(WATCHERS)); } public void addWatchers(Collection<Watcher> watchers) { storage.get(WATCHERS).addAll(watchers); } /** * Don't forget to use Include.children flag when loading issue from Redmine server: * <pre> * Issue issue = issueManager.getIssueById(3205, Include.children); * </pre> * @return Collection of entries or empty collection if no objects found. * @see com.taskadapter.redmineapi.Include#children */ public Collection<Issue> getChildren() { return Collections.unmodifiableCollection(storage.get(CHILDREN)); } public void addChildren(Collection<Issue> children) { storage.get(CHILDREN).addAll(children); } /** * Issues are considered equal if their IDs are equal. what about two issues with null ids? */ @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Issue issue = (Issue) o; if (getId() != null ? !getId().equals(issue.getId()) : issue.getId() != null) return false; return true; } @Override public int hashCode() { return getId() != null ? getId().hashCode() : 0; } /** * @return the custom field with given Id or NULL if the field is not found */ public CustomField getCustomFieldById(int customFieldId) { for (CustomField customField : storage.get(CUSTOM_FIELDS)) { if (customFieldId == customField.getId()) { return customField; } } return null; } /** * @return the custom field with given name or NULL if the field is not found */ public CustomField getCustomFieldByName(String customFieldName) { for (CustomField customField : storage.get(CUSTOM_FIELDS)) { if (customFieldName.equals(customField.getName())) { return customField; } } return null; } @Override public String toString() { return "Issue [id=" + getId() + ", subject=" + getSubject() + "]"; } /** * Relations are only loaded if you include Include.relations when loading the Issue. * <pre> * Issue issue = issueManager.getIssueById(3205, Include.relations); * </pre> * <p>Since the returned collection is not modifiable, you need to use addRelations() method * if you want to add elements, e.g.: * <pre> * issue.addRelations(Collections.singletonList(relation)); * </pre> * @return unmodifiable collection of Relations or EMPTY collection if none found. Never returns NULL. * @see com.taskadapter.redmineapi.Include#relations */ public Collection<IssueRelation> getRelations() { return Collections.unmodifiableCollection(storage.get(RELATIONS)); } public void addRelations(Collection<IssueRelation> collection) { storage.get(RELATIONS).addAll(collection); } public Integer getPriorityId() { return storage.get(PRIORITY_ID); } public void setPriorityId(Integer priorityId) { storage.set(PRIORITY_ID, priorityId); } public Version getTargetVersion() { return storage.get(TARGET_VERSION); } /** * Don't forget to use <i>Include.attachments</i> flag when loading issue from Redmine server: * <pre> * Issue issue = issueManager.getIssueById(3205, Include.attachments); * </pre> * @return unmodifiable collection of entries or empty collection if no objects found. * @see com.taskadapter.redmineapi.Include#attachments */ public Collection<Attachment> getAttachments() { return Collections.unmodifiableCollection(storage.get(ATTACHMENTS)); } public void addAttachments(Collection<Attachment> collection) { storage.get(ATTACHMENTS).addAll(collection); } public void addAttachment(Attachment attachment) { storage.get(ATTACHMENTS).add(attachment); } public void setTargetVersion(Version version) { storage.set(TARGET_VERSION, version); } public IssueCategory getCategory() { return storage.get(ISSUE_CATEGORY); } public void setCategory(IssueCategory category) { storage.set(ISSUE_CATEGORY, category); } /** * Default value is not determines. it's up to the server what it thinks the default value is if not set. */ public boolean isPrivateIssue() { return storage.get(PRIVATE_ISSUE); } public void setPrivateIssue(boolean privateIssue) { storage.set(PRIVATE_ISSUE, privateIssue); } public PropertyStorage getStorage() { return storage; } }