public class NotFoundException extends RedmineException {

    private static final long serialVersionUID = 1L;

    public NotFoundException(String msg) {
        super(msg);
    }
} public class RedmineAuthenticationException extends RedmineSecurityException {
	private static final long serialVersionUID = -2494397318821827279L;

	public RedmineAuthenticationException(String message) {
        super(message);
    }
} public class RedmineException extends Exception {
	private static final long serialVersionUID = -1592189045756043062L;

	public RedmineException() {
    }

    public RedmineException(String message, Throwable cause) {
        super(message, cause);
    }

    public RedmineException(String message) {
        super(message);
    }

    public RedmineException(Throwable cause) {
        super(cause);
    }
} public class RedmineFormatException extends RedmineCommunicationException {
	private static final long serialVersionUID = 4024202727798727085L;

	public RedmineFormatException(String message) {
		super(message);
	}

	public RedmineFormatException(Throwable cause) {
        super(cause);
    }

	public RedmineFormatException(String message, Throwable cause) {
		super(message, cause);
	}
} public class RedmineInternalError extends RuntimeException {

    private static final long serialVersionUID = 1L;

    public RedmineInternalError() {
    }

    public RedmineInternalError(String message, Throwable cause) {
        super(message, cause);
    }

    public RedmineInternalError(String message) {
        super(message);
    }

    public RedmineInternalError(Throwable cause) {
        super(cause);
    }

} public class RedmineManager {

    private final Transport transport;
    private final IssueManager issueManager;
    private final AttachmentManager attachmentManager;
    private final UserManager userManager;
    private final ProjectManager projectManager;
    private final MembershipManager membershipManager;
    private final CustomFieldManager customFieldManager;
    private final WikiManager wikiManager;
    private final TimeEntryManager timeEntryManager;

    RedmineManager(Transport transport) {
        this.transport = transport;
        issueManager = new IssueManager(transport);
        attachmentManager = new AttachmentManager(transport);
        userManager = new UserManager(transport);
        projectManager = new ProjectManager(transport);
        membershipManager = new MembershipManager(transport);
        wikiManager = new WikiManager(transport);
        customFieldManager = new CustomFieldManager(transport);
        timeEntryManager = new TimeEntryManager(transport);
    }

    public WikiManager getWikiManager() {
        return wikiManager;
    }

    public IssueManager getIssueManager() {
        return issueManager;
    }

    public AttachmentManager getAttachmentManager() {
        return attachmentManager;
    }

    public UserManager getUserManager() {
        return userManager;
    }

    public ProjectManager getProjectManager() {
        return projectManager;
    }

    /**
     * DEPRECATED - all methods from MembershipManager were migrated to ProjectManager class
     */
    @Deprecated
    public MembershipManager getMembershipManager() {
        return membershipManager;
    }

    public CustomFieldManager getCustomFieldManager() {
        return customFieldManager;
    }

    public TimeEntryManager getTimeEntryManager() {
        return timeEntryManager;
    }

    /**
     * This number of objects (tasks, projects, users) will be requested from Redmine server in 1 request.
     * Note that if you set objects per page to be, say, 10, and you have 20 objects on the server,
     * then issueManager.getIssues() will still return ALL 20 objects at once even though the library
     * will perform 2 requests to the server (to load 10 objects each time).
     */
    public void setObjectsPerPage(int pageSize) {
		transport.setObjectsPerPage(pageSize);
    }

    /**
     * This works only when the main authentication has led to Redmine Admin level user.
     * The given user name will be sent to the server in "X-Redmine-Switch-User" HTTP Header
     * to indicate that the action (create issue, delete issue, etc) must be done
     * on behalf of the given user name.
     *
     * @param loginName Redmine user login name to provide to the server
     *
     * @see <a href="http://www.redmine.org/issues/11755">Redmine issue 11755</a>
     */
    public void setOnBehalfOfUser(String loginName) {
        transport.setOnBehalfOfUser(loginName);
    }

    public Transport getTransport() {
        return transport;
    }
} public class Attachment implements Identifiable, FluentStyle {

    private final PropertyStorage storage = new PropertyStorage();

    /**
     * database numeric Id
     */
    public final static Property<Integer> DATABASE_ID = new Property<>(Integer.class, "id");
    public final static Property<String> FILE_NAME = new Property<>(String.class, "fileName");
    public final static Property<Long> FILE_SIZE = new Property<>(Long.class, "fileSize");
    public final static Property<String> CONTENT_TYPE = new Property<>(String.class, "contentType");
    public final static Property<String> CONTENT_URL = new Property<>(String.class, "contentURL");
    public final static Property<String> DESCRIPTION = new Property<>(String.class, "description");
    public final static Property<Date> CREATED_ON = new Property<>(Date.class, "createdOn");
    public final static Property<User> AUTHOR = new Property<>(User.class, "author");
    public final static Property<String> TOKEN = new Property<>(String.class, "token");
    private Transport transport;

    public Attachment(Transport transport) {
        setTransport(transport);
    }

    public Attachment setId(Integer id) {
        storage.set(DATABASE_ID, id);
        return this;
    }

    /**
     * @return id. NULL for attachments not added to Redmine yet.
     */
    @Override
    public Integer getId() {
        return storage.get(DATABASE_ID);
    }

    public String getContentType() {
        return storage.get(CONTENT_TYPE);
    }

    public Attachment setContentType(String contentType) {
        storage.set(CONTENT_TYPE, contentType);
        return this;
    }

    public String getContentURL() {
        return storage.get(CONTENT_URL);
    }

    public Attachment setContentURL(String contentURL) {
        storage.set(CONTENT_URL, contentURL);
        return this;
    }

    /**
     * Description is empty by default, not NULL.
     */
    public String getDescription() {
        return storage.get(DESCRIPTION);
    }

    public Attachment setDescription(String description) {
        storage.set(DESCRIPTION, description);
        return this;
    }

    public Date getCreatedOn() {
        return storage.get(CREATED_ON);
    }

    public Attachment setCreatedOn(Date createdOn) {
        storage.set(CREATED_ON, createdOn);
        return this;
    }

    public User getAuthor() {
        return storage.get(AUTHOR);
    }

    public Attachment setAuthor(User author) {
        storage.set(AUTHOR, author);
        return this;
    }

    public String getFileName() {
        return storage.get(FILE_NAME);
    }

    public Attachment setFileName(String fileName) {
        storage.set(FILE_NAME, fileName);
        return this;
    }

    public Long getFileSize() {
        return storage.get(FILE_SIZE);
    }

    public Attachment setFileSize(Long fileSize) {
        storage.set(FILE_SIZE, fileSize);
        return this;
    }

    public String getToken() {
        return storage.get(TOKEN);
    }

    public Attachment setToken(String token) {
        storage.set(TOKEN, token);
        return this;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Attachment that = (Attachment) o;

        if (getId() != null ? !getId().equals(that.getId()) : that.getId() != null) return false;
        if (getToken() != null ? !getToken().equals(that.getToken()) : that.getToken() != null) return false;

        return true;
    }

    @Override
    public int hashCode() {
        int hash = 5;
        hash = 3 * hash + (getId() != null ? getId().hashCode() : 0);
        hash = 3 * hash + (getToken() != null ? getToken().hashCode() : 0);
        return hash;
    }

    @Override
    public String toString() {
        return "Attachment{" +
                "id=" + getId() +
                ", fileName='" + getFileName() + '\'' +
                ", fileSize=" + getFileSize() +
                ", contentType='" + getContentType() + '\'' +
                ", contentURL='" + getContentURL() + '\'' +
                ", description='" + getDescription() + '\'' +
                ", createdOn=" + getCreatedOn() +
                ", author=" + getAuthor() +
                ", token=" + getToken() +
                '}';
    }

    public PropertyStorage getStorage() {
        return storage;
    }

    /**
     * delete the attachment with pre-configured ID from the server.
     * <br>
     * see http://www.redmine.org/issues/14828
     *
     * @since Redmine 3.3.0
     */
    public void delete() throws RedmineException {
        transport.deleteObject(Attachment.class, Integer.toString(getId()));
    }

    @Override
    public void setTransport(Transport transport) {
        this.transport = transport;
        PropertyStorageUtil.updateCollections(storage, transport);
    }
} public class CustomFieldDefinition {

    private Integer id;
    private String name;
    private String customizedType;
    private String fieldFormat;
    private String regexp;
    private Integer minLength;
    private Integer maxLength;
    private boolean required;
    private boolean filter;
    private boolean searchable;
    private boolean multiple;
    private String defaultValue;
    private boolean visible;
    private final List<String> possibleValues = new ArrayList<>();
    private final List<Tracker> trackers = new ArrayList<>();
    private final List<Role> roles = new ArrayList<>();

    public Integer getId() {
        return id;
    }

    /**
     * @param id database ID.
     */
    public CustomFieldDefinition setId(Integer id) {
        this.id = id;
        return this;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getCustomizedType() {
        return customizedType;
    }

    public void setCustomizedType(String customizedType) {
        this.customizedType = customizedType;
    }

    public String getFieldFormat() {
        return fieldFormat;
    }

    public void setFieldFormat(String fieldFormat) {
        this.fieldFormat = fieldFormat;
    }

    public String getRegexp() {
        return regexp;
    }

    public void setRegexp(String regexp) {
        this.regexp = regexp;
    }

    public Integer getMinLength() {
        return minLength;
    }

    public void setMinLength(Integer minLength) {
        this.minLength = minLength;
    }

    public Integer getMaxLength() {
        return maxLength;
    }

    public void setMaxLength(Integer maxLength) {
        this.maxLength = maxLength;
    }

    public boolean isRequired() {
        return required;
    }

    public void setRequired(boolean required) {
        this.required = required;
    }

    public boolean isFilter() {
        return filter;
    }

    public void setFilter(Boolean filter) {
        this.filter = filter;
    }

    public boolean isSearchable() {
        return searchable;
    }

    public void setSearchable(Boolean searchable) {
        this.searchable = searchable;
    }

    public boolean isMultiple() {
        return multiple;
    }

    public void setMultiple(Boolean multiple) {
        this.multiple = multiple;
    }

    public String getDefaultValue() {
        return defaultValue;
    }

    public void setDefaultValue(String defaultValue) {
        this.defaultValue = defaultValue;
    }

    public boolean isVisible() {
        return visible;
    }

    public void setVisible(Boolean visible) {
        this.visible = visible;
    }

    public List<String> getPossibleValues() {
        return possibleValues;
    }

    public List<Tracker> getTrackers() {
        return trackers;
    }

    public List<Role> getRoles() {
        return roles;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        CustomFieldDefinition that = (CustomFieldDefinition) o;

        if (id != null ? !id.equals(that.id) : that.id != null) return false;

        return true;
    }

    @Override
    public int hashCode() {
        return id != null ? id.hashCode() : 0;
    }

    @Override
    public String toString() {
        return "CustomFieldDefinition{" + "id=" + id + ", name=" + name +
                ", customizedType=" + customizedType + ", fieldFormat=" +
                fieldFormat + ", regexp=" + regexp + ", minLength=" + minLength +
                ", maxLength=" + maxLength + ", required=" + required +
                ", filter=" + filter + ", searchable=" + searchable +
                ", multiple=" + multiple + ", defaultValue=" + defaultValue +
                ", visible=" + visible + ", possibleValues=" + possibleValues +
                ", trackers=" + trackers + ", roles=" + roles + '}';
    }
} public class File implements Identifiable, FluentStyle {

  private final PropertyStorage storage = new PropertyStorage();

  /**
   * database numeric Id
   */
  public final static Property<Integer> DATABASE_ID = new Property<>(Integer.class, "id");
  public final static Property<String> FILENAME = new Property<>(String.class, "filename");
  public final static Property<Long> FILESIZE = new Property<>(Long.class, "filesize");
  public final static Property<String> CONTENT_TYPE = new Property<>(String.class, "content_type");
  public final static Property<String> DESCRIPTION = new Property<>(String.class, "description");
  public final static Property<String> CONTENT_URL = new Property<>(String.class, "content_url");
  public final static Property<User> AUTHOR = new Property<>(User.class, "author");
  public final static Property<Date> CREATED_ON = new Property<>(Date.class, "created_on");
  public final static Property<Version> VERSION = new Property<>(Version.class, "version");
  public final static Property<String> DIGEST = new Property<>(String.class, "digest");
  public final static Property<Integer> DOWNLOADS = new Property<>(Integer.class, "downloads");
  public final static Property<String> TOKEN = new Property<>(String.class, "token");

  private Transport transport;

  public File(Transport transport) {
    setTransport(transport);
  }

  @Override
  public void setTransport(Transport transport) {
    this.transport = transport;
    PropertyStorageUtil.updateCollections(storage, transport);
  }

  public File setId(Integer id) {
    storage.set(DATABASE_ID, id);
    return this;
  }

  @Override
  public Integer getId() {
    return storage.get(DATABASE_ID);
  }

  public String getContentType() {
    return storage.get(CONTENT_TYPE);
  }

  public File setContentType(String contentType) {
    storage.set(CONTENT_TYPE, contentType);
    return this;
  }

  public String getContentURL() {
    return storage.get(CONTENT_URL);
  }

  public File setContentURL(String contentURL) {
    storage.set(CONTENT_URL, contentURL);
    return this;
  }

  /**
   * Description is empty by default, not NULL.
   */
  public String getDescription() {
    return storage.get(DESCRIPTION);
  }

  public File setDescription(String description) {
    storage.set(DESCRIPTION, description);
    return this;
  }

  public Date getCreatedOn() {
    return storage.get(CREATED_ON);
  }

  public File setCreatedOn(Date createdOn) {
    storage.set(CREATED_ON, createdOn);
    return this;
  }

  public User getAuthor() {
    return storage.get(AUTHOR);
  }

  public File setAuthor(User author) {
    storage.set(AUTHOR, author);
    return this;
  }

  public String getFileName() {
    return storage.get(FILENAME);
  }

  public File setFileName(String fileName) {
    storage.set(FILENAME, fileName);
    return this;
  }

  public Long getFileSize() {
    return storage.get(FILESIZE);
  }

  public File setFileSize(Long fileSize) {
    storage.set(FILESIZE, fileSize);
    return this;
  }

  public Version getVersion() {
    return storage.get(VERSION);
  }

  public File setVersion(Version version) {
    storage.set(VERSION, version);
    return this;
  }

  public String getDigest() {
    return storage.get(DIGEST);
  }

  public File setDigest(String digest) {
    storage.set(DIGEST, digest);
    return this;
  }

  public int getDownloads() {
    return storage.get(DOWNLOADS);
  }

  public File setDownloads(int downloads) {
    storage.set(DOWNLOADS, downloads);
    return this;
  }

  public String getToken() {
    return storage.get(TOKEN);
  }

  public File setToken(String token) {
    storage.set(TOKEN, token);
    return this;
  }

  public PropertyStorage getStorage() {
    return storage;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    File that = (File) o;

    if (getId() != null ? !getId().equals(that.getId()) : that.getId() != null) return false;
    if (getToken() != null ? !getToken().equals(that.getToken()) : that.getToken() != null) return false;

    return true;
  }

  @Override
  public int hashCode() {
    int hash = 5;
    hash = 3 * hash + (getId() != null ? getId().hashCode() : 0);
    hash = 3 * hash + (getToken() != null ? getToken().hashCode() : 0);
    return hash;
  }

  @Override
  public String toString() {
    return "File{" +
        "id=" + getId() +
        ", fileName='" + getFileName() + '\'' +
        ", fileSize=" + getFileSize() +
        ", contentType='" + getContentType() + '\'' +
        ", contentURL='" + getContentURL() + '\'' +
        ", description='" + getDescription() + '\'' +
        ", createdOn=" + getCreatedOn() +
        ", author=" + getAuthor() +
        ", token=" + getToken() +
        '}';
  }
} public interface FluentStyle {
    void setTransport(Transport transport);
} public class Group implements Identifiable, FluentStyle {

    private final PropertyStorage storage = new PropertyStorage();

    public final static Property<Integer> ID = new Property<Integer>(Integer.class, "id");
    public final static Property<String> NAME = new Property<String>(String.class, "name");
    private Transport transport;

    public Group(Transport transport) {
        setTransport(transport);
    }

    /**
     * @param id database ID of the group
     */
    public Group setId(Integer id) {
        storage.set(ID, id);
        return this;
    }

    @Override
    public Integer getId() {
        return storage.get(ID);
    }

    public String getName() {
        return storage.get(NAME);
    }

    public Group setName(String name) {
        storage.set(NAME, name);
        return this;
    }

    @Override
    public String toString() {
        return "Group{" +
                "id=" + getId() +
                ", name='" + getName() + '\'' +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Group group = (Group) o;

        if (getId() != null ? !getId().equals(group.getId()) : group.getId() != null) return false;
        return true;
    }

    @Override
    public int hashCode() {
        return getId() != null ? getId().hashCode() : 0;
    }

    public PropertyStorage getStorage() {
        return storage;
    }

    /**
     * Creates a new group.
     * <p><strong>This operation requires "Redmine Administrator" permission.</strong>
     *
     * @return created group.
     */
    public Group create() throws RedmineException {
        return transport.addObject(this);
    }

    public void update() throws RedmineException {
        transport.updateObject(this);
    }

    /**
     * Delete this group. The object must have Id set.
     * <p><strong>This operation requires "Redmine Administrator" permission.</strong>
     */
    public void delete() throws RedmineException {
        transport.deleteObject(Group.class, getId().toString());
    }

    @Override
    public void setTransport(Transport transport) {
        this.transport = transport;
    }
} public interface Identifiable {
    Integer getId();
} public class Issue implements Identifiable, FluentStyle {

    private final PropertyStorage storage = new PropertyStorage();

    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");

    private Transport transport;

    public Issue() {
        initCollections(storage);
    }

    public Issue(Transport transport) {
        this();
        setTransport(transport);
    }

    /**
     * Each Issue object must have project Id set in order for Redmine 3.x to accept it via REST API.
     */
    public Issue(Transport transport, int projectId) {
        this();
        this.transport = transport;
        setProjectId(projectId);
    }

    /**
     * @param projectId Each Issue object must have project Id set in order for Redmine 3.x to accept it via REST API.
     */
    public Issue(Transport transport, int projectId, String subject) {
        this();
        this.transport = transport;
        setSubject(subject);
        setProjectId(projectId);
    }

    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 Issue setProjectId(Integer projectId) {
        storage.set(PROJECT_ID, projectId);
        return this;
    }

    public String getProjectName() {
        return storage.get(PROJECT_NAME);
    }


    public Issue setProjectName(String name) {
        storage.set(PROJECT_NAME, name);
        return this;
    }

    /**
     * @param id database ID.
     */
    public Issue setId(Integer id) {
        storage.set(DATABASE_ID, id);
        return this;
    }

    public Integer getDoneRatio() {
        return storage.get(DONE_RATIO);
    }

    public Issue setDoneRatio(Integer doneRatio) {
        storage.set(DONE_RATIO, doneRatio);
        return this;
    }

    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 Issue setAssigneeId(Integer assigneeId) {
        storage.set(ASSIGNEE_ID, assigneeId);
        return this;
    }

    public String getAssigneeName() {
        return storage.get(ASSIGNEE_NAME);
    }

    public Issue setAssigneeName(String assigneeName) {
        storage.set(ASSIGNEE_NAME, assigneeName);
        return this;
    }

    public Float getEstimatedHours() {
        return storage.get(ESTIMATED_HOURS);
    }

    public Issue setEstimatedHours(Float estimatedTime) {
        storage.set(ESTIMATED_HOURS, estimatedTime);
        return this;
    }

    public Float getSpentHours() {
        return storage.get(SPENT_HOURS);
    }

    public Issue setSpentHours(Float spentHours) {
        storage.set(SPENT_HOURS, spentHours);
        return this;
    }

  /**
     * 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 Issue setParentId(Integer parentId) {
        storage.set(PARENT_ID, parentId);
        return this;
    }

    /**
     * @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 Issue setSubject(String subject) {
        storage.set(SUBJECT, subject);
        return this;
    }

    public Date getStartDate() {
        return storage.get(START_DATE);
    }

    public Issue setStartDate(Date startDate) {
        storage.set(START_DATE, startDate);
        return this;
    }

    public Date getDueDate() {
        return storage.get(DUE_DATE);
    }

    public Issue setDueDate(Date dueDate) {
        storage.set(DUE_DATE, dueDate);
        return this;
    }

    public Integer getAuthorId() {
        return storage.get(AUTHOR_ID);
    }

    /**
     * Marking as "deprecated": according to Redmine REST API docs
     * https://www.redmine.org/projects/redmine/wiki/Rest_Issues#Creating-an-issue , this parameter is not used
     * when creating issues (January 2020).
     */
    @Deprecated
    public Issue setAuthorId(Integer id) {
        storage.set(AUTHOR_ID, id);
        return this;
    }

    public String getAuthorName() {
        return storage.get(AUTHOR_NAME);
    }

    /**
     * Marking as "deprecated": according to Redmine REST API docs
     * https://www.redmine.org/projects/redmine/wiki/Rest_Issues#Creating-an-issue , this parameter is not used
     * when creating issues (January 2020).
     */
    @Deprecated
    public Issue setAuthorName(String name) {
        storage.set(AUTHOR_NAME, name);
        return this;
    }

    public Tracker getTracker() {
        return storage.get(TRACKER);
    }

    public Issue setTracker(Tracker tracker) {
        storage.set(TRACKER, tracker);
        return this;
    }

    public String getDescription() {
        return storage.get(DESCRIPTION);
    }

    public Issue setDescription(String description) {
        storage.set(DESCRIPTION, description);
        return this;
    }

    public Date getCreatedOn() {
        return storage.get(CREATED_ON);
    }

    public Issue setCreatedOn(Date createdOn) {
        storage.set(CREATED_ON, createdOn);
        return this;
    }

    public Date getUpdatedOn() {
        return storage.get(UPDATED_ON);
    }

    public Issue setUpdatedOn(Date updatedOn) {
        storage.set(UPDATED_ON, updatedOn);
        return this;
    }

    public Date getClosedOn() {
        return storage.get(CLOSED_ON);
    }

    public Issue setClosedOn(Date closedOn) {
        storage.set(CLOSED_ON, closedOn);
        return this;
    }

    public Integer getStatusId() {
        return storage.get(STATUS_ID);
    }

    public Issue setStatusId(Integer statusId) {
        storage.set(STATUS_ID, statusId);
        return this;
    }

    public String getStatusName() {
        return storage.get(STATUS_NAME);
    }

    public Issue setStatusName(String statusName) {
        storage.set(STATUS_NAME, statusName);
        return this;
    }

    /**
     * @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 Issue clearCustomFields() {
        storage.set(CUSTOM_FIELDS, new HashSet<>());
        return this;
    }

    /**
     * NOTE: The custom field(s) <strong>must have correct database ID set</strong> to be saved to Redmine.
     * This is Redmine REST API's requirement.
     */
    public Issue addCustomFields(Collection<CustomField> customFields) {
        storage.get(CUSTOM_FIELDS).addAll(customFields);
        return this;
    }

    /**
     * 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 Issue addCustomField(CustomField customField) {
        storage.get(CUSTOM_FIELDS).add(customField);
        return this;
    }

    @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 Issue setNotes(String notes) {
        storage.set(NOTES, notes);
        return this;
    }

    public boolean isPrivateNotes() {
        return storage.get(PRIVATE_NOTES);
    }

    /**
     * @param privateNotes mark note as private
     */
    public Issue setPrivateNotes(boolean privateNotes) {
        storage.set(PRIVATE_NOTES, privateNotes);
        return this;
    }

    /**
     * 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 Issue addChangesets(Collection<Changeset> changesets) {
        storage.get(CHANGESETS).addAll(changesets);
        return this;
    }

    /**
     * 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 Issue addWatchers(Collection<Watcher> watchers) {
        storage.get(WATCHERS).addAll(watchers);
        return this;
    }

    /**
      * 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 Issue addChildren(Collection<Issue> children) {
        storage.get(CHILDREN).addAll(children);
        return this;
    }

    /**
     * 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 Issue addRelations(Collection<IssueRelation> collection) {
        storage.get(RELATIONS).addAll(collection);
        return this;
    }

    public Integer getPriorityId() {
        return storage.get(PRIORITY_ID);
    }

    public Issue setPriorityId(Integer priorityId) {
        storage.set(PRIORITY_ID, priorityId);
        return this;
    }

    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 Issue addAttachments(Collection<Attachment> collection) {
        storage.get(ATTACHMENTS).addAll(collection);
        return this;
    }

    public Issue addAttachment(Attachment attachment) {
        storage.get(ATTACHMENTS).add(attachment);
        return this;
    }

    public Issue setTargetVersion(Version version) {
        storage.set(TARGET_VERSION, version);
        return this;
    }

    public IssueCategory getCategory() {
        return storage.get(ISSUE_CATEGORY);
    }

    public Issue setCategory(IssueCategory category) {
        storage.set(ISSUE_CATEGORY, category);
        return this;
    }

    /**
     * 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 Issue setPrivateIssue(boolean privateIssue) {
        storage.set(PRIVATE_ISSUE, privateIssue);
        return this;
    }

    public PropertyStorage getStorage() {
        return storage;
    }

    /**
     * @return the newly created Issue.
     *
     * @throws RedmineAuthenticationException invalid or no API access key is used with the server, which
     *                                 requires authorization. Check the constructor arguments.
     * @throws NotFoundException       the required project is not found
     * @throws RedmineException
     */
    public Issue create(RequestParam... params) throws RedmineException {
        RequestParam[] enrichParams = Arrays.copyOf(params, params.length + 1);
        enrichParams[params.length] = new RequestParam("include",
                Include.attachments.toString());
        return transport.addObject(this, enrichParams);
    }

    public void update(RequestParam... params) throws RedmineException {
        transport.updateObject(this, params);
    }

    public void delete() throws RedmineException {
        transport.deleteObject(Issue.class, Integer.toString(this.getId()));
    }

    public void addWatcher(int watcherId) throws RedmineException {
        transport.addWatcherToIssue(watcherId, getId());
    }

    public void deleteWatcher(int watcherId) throws RedmineException {
        transport.deleteChildId(Issue.class, Integer.toString(getId()), new Watcher(), watcherId);
    }

    @Override
    public void setTransport(Transport transport) {
        this.transport = transport;
        PropertyStorageUtil.updateCollections(storage, transport);
    }
} public class IssueCategory implements Identifiable, Serializable, FluentStyle {
	private static final long serialVersionUID = -109010410391968475L;

    private final PropertyStorage storage = new PropertyStorage();

    /**
     * database numeric ID.
     */
    public final static Property<Integer> DATABASE_ID = new Property<>(Integer.class, "id");
    public final static Property<String> NAME = new Property<>(String.class, "name");
    public final static Property<Integer> PROJECT_ID = new Property<>(Integer.class, "projectId");
    public final static Property<Integer> ASSIGNEE_ID = new Property<>(Integer.class, "assigneeId");
    public final static Property<String> ASSIGNEE_NAME = new Property<>(String.class, "assigneeName");
    private Transport transport;

    public IssueCategory(Transport transport) {
        setTransport(transport);
    }

    public IssueCategory(Transport transport, Integer projectId, String name) {
        setTransport(transport);
        setProjectId(projectId);
        setName(name);
    }

    public Integer getId() {
        return storage.get(DATABASE_ID);
    }

    public String getName() {
        return storage.get(NAME);
    }

    public IssueCategory setName(String name) {
        storage.set(NAME, name);
        return this;
    }

    /**
     * @param id database Id of the issue category.
     */
    public IssueCategory setId(Integer id) {
        storage.set(DATABASE_ID, id);
        return this;
    }

    public Integer getProjectId() {
        return storage.get(PROJECT_ID);
    }

    public IssueCategory setProjectId(Integer projectId) {
        storage.set(PROJECT_ID, projectId);
        return this;
    }

    /**
     * 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 IssueCategory setAssigneeId(Integer assigneeId) {
        storage.set(ASSIGNEE_ID, assigneeId);
        return this;
    }

    public String getAssigneeName() {
        return storage.get(ASSIGNEE_NAME);
    }

    public void setAssigneeName(String assigneeName) {
        storage.set(ASSIGNEE_NAME, assigneeName);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        IssueCategory that = (IssueCategory) o;

        if (getId() != null ? !getId().equals(that.getId()) : that.getId() != null) return false;

        return true;
    }

    @Override
    public int hashCode() {
        return getId() != null ? getId().hashCode() : 0;
    }

    @Override
    public String toString() {
        return "IssueCategory{" +
                "id=" + getId() +
                ", name='" + getName() + '\'' +
                ", projectId=" + getProjectId() +
                ", assigneeId=" + getAssigneeId() +
                ", assigneeName='" + getAssigneeName() + '\'' +
                '}';
    }

    public PropertyStorage getStorage() {
        return storage;
    }

    @Override
    public void setTransport(Transport transport) {
        this.transport = transport;
    }

    /**
     * creates a new {@link IssueCategory} for the {@link Project} contained. <br>
     * Pre-condition: the attribute {@link Project} for the {@link IssueCategory} must
     * not be null!
     *
     * @return the new {@link IssueCategory} created by Redmine
     * @throws IllegalArgumentException       thrown in case the category does not contain a project.
     * @throws RedmineAuthenticationException thrown in case something went wrong while trying to login
     * @throws RedmineException               thrown in case something went wrong in Redmine
     * @throws NotFoundException              thrown in case an object can not be found
     */
    public IssueCategory create() throws RedmineException {
        if (getProjectId() == null) {
            throw new IllegalArgumentException(
                    "IssueCategory must contain projectId");
        }

        return transport.addChildEntry(Project.class, getProjectId().toString(), this);
    }

    public void delete() throws RedmineException {
        transport.deleteObject(IssueCategory.class, Integer.toString(this.getId()));
    }
} public class IssuePriority {
    /**
     * database ID.
     */
    private Integer id;
    private String name;
    private boolean isDefault;

    /**
     * @param id database ID.
     */
    public IssuePriority setId(Integer id) {
        this.id = id;
        return this;
    }

    public Integer getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public IssuePriority setName(String name) {
        this.name = name;
        return this;
    }

    public boolean isDefault() {
        return isDefault;
    }

    public IssuePriority setDefault(boolean isDefault) {
        this.isDefault = isDefault;
        return this;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        IssuePriority that = (IssuePriority) o;

        if (id != null ? !id.equals(that.id) : that.id != null) return false;

        return true;
    }

    @Override
    public int hashCode() {
        return id != null ? id.hashCode() : 0;
    }

    @Override
    public String toString() {
        return "Priority [id=" + id + ", name=" + name + ", isDefault="
                + isDefault + "]";
    }

} public class IssueRelation implements Identifiable, FluentStyle {
    private Transport transport;

    public enum TYPE {
        precedes
    }

    /*
     GET /relations/1819.xml
     Response:

     <?xml version="1.0" encoding="UTF-8"?>
     <relation>
       <id>1819</id>
       <issue_id>8470</issue_id>
       <issue_to_id>8469</issue_to_id>
       <relation_type>relates</relation_type>
       <delay/>
     </relation>
     */

    private final PropertyStorage storage = new PropertyStorage();

    /**
     * database numeric Id
     */
    public final static Property<Integer> DATABASE_ID = new Property<>(Integer.class, "id");
    public final static Property<Integer> ISSUE_ID = new Property<>(Integer.class, "issueId");
    public final static Property<List<Integer>> ISSUE_TO_ID = new Property(List.class, "issueToId");

    public final static Property<String> RELATION_TYPE = new Property<>(String.class, "relationType");
    public final static Property<Integer> DELAY = new Property<>(Integer.class, "delay");

    private IssueRelation() {
        storage.set(ISSUE_TO_ID, new ArrayList<>());
    }

    public IssueRelation(Transport transport) {
        this();
        setTransport(transport);
    }

    public IssueRelation(Transport transport, Integer issueId, Integer issueToId, String type) {
        this();
        setTransport(transport);
        setIssueId(issueId);
        addIssueToId(issueToId);
        setType(type);
    }

    public IssueRelation setId(Integer id) {
        storage.set(DATABASE_ID, id);
        return this;
    }

    @Override
    public Integer getId() {
        return storage.get(DATABASE_ID);
    }

    public Integer getIssueId() {
        return storage.get(ISSUE_ID);
    }

    public IssueRelation setIssueId(Integer issueId) {
        storage.set(ISSUE_ID, issueId);
        return this;
    }

    public Integer getIssueToId() {
        return storage.get(ISSUE_TO_ID)
                .stream()
                .findFirst().orElse(null);
    }

    public IssueRelation addIssueToId(Integer issueToId) {
        storage.get(ISSUE_TO_ID).add(issueToId);
        return this;
    }

    public Integer getDelay() {
        return storage.get(DELAY);
    }

    public IssueRelation setDelay(Integer delay) {
        storage.set(DELAY, delay);
        return this;
    }

    public String getType() {
        return storage.get(RELATION_TYPE);
    }

    public IssueRelation setType(String type) {
        storage.set(RELATION_TYPE, type);
        return this;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        IssueRelation that = (IssueRelation) o;

        if (getId() != null ? !getId().equals(that.getId()) : that.getId() != null) return false;

        return true;
    }

    @Override
    public int hashCode() {
        return getId() != null ? getId().hashCode() : 0;
    }

    @Override
    public String toString() {
        return "IssueRelation [getId()=" + getId() + ", issueId=" + getIssueId()
                + ", issueToId=" + getIssueToId() + ", type=" + getType() + ", delay="
                + getDelay() + "]";
    }

    public PropertyStorage getStorage() {
        return storage;
    }

    @Override
    public void setTransport(Transport transport) {
        this.transport = transport;
    }

    /**
     * Each relation must have issueId, issueToId and type set.
     */
    public IssueRelation create() throws RedmineException {
        return transport.addChildEntry(Issue.class, getIssueId().toString(), this);
    }

    public void delete() throws RedmineException {
        transport.deleteObject(IssueRelation.class, Integer.toString(getId()));
    }

} public class IssueStatus implements Identifiable, Serializable {
	private static final long serialVersionUID = -2221390098554222099L;

	private Integer id;
    private String name;
    private boolean defaultStatus = false;
    private boolean closed = false;

    public IssueStatus setId(Integer id) {
        this.id = id;
        return this;
    }

    public Integer getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public IssueStatus setName(String name) {
        this.name = name;
        return this;
    }

    public boolean isDefaultStatus() {
        return defaultStatus;
    }

    public void setDefaultStatus(boolean defaultStatus) {
        this.defaultStatus = defaultStatus;
    }

    public boolean isClosed() {
        return closed;
    }

    public void setClosed(boolean closed) {
        this.closed = closed;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        IssueStatus that = (IssueStatus) o;

        if (id != null ? !id.equals(that.id) : that.id != null) return false;

        return true;
    }

    @Override
    public int hashCode() {
        return id != null ? id.hashCode() : 0;
    }

    @Override
    public String toString() {
        return "Status [id=" + id + ", name=" + name + ", isDefault=" + defaultStatus + ", closed=" + closed + "]";
    }

} public class Membership implements Identifiable, FluentStyle {

	private final PropertyStorage storage= new PropertyStorage();

	/**
	 * database numeric ID.
	 */
	public final static Property<Integer> DATABASE_ID = new Property<>(Integer.class, "id");
	public final static Property<Project> PROJECT = new Property<>(Project.class, "project");
	/**
	 * User. Not set for "group" membership.
	 */
	public final static Property<Integer> USER_ID = new Property<>(Integer.class, "userId");
        public final static Property<String> USER_NAME = new Property<>(String.class, "userName");
        /**
         * Group. Not set for "user" membership.
         */
	public final static Property<Integer> GROUP_ID = new Property<>(Integer.class, "groupId");
        public final static Property<String> GROUP_NAME = new Property<>(String.class, "groupName");
	public final static Property<Set<Role>> ROLES = (Property<Set<Role>>) new Property(Set.class, "roles");
	private Transport transport;

	public Membership(Transport transport) {
		storage.set(ROLES, new HashSet<>());
		setTransport(transport);
	}

	public Membership(Transport transport, Project project, int userId) {
		this(transport);
		setProject(project);
		setUserId(userId);
	}

    /**
     * @param id database ID.
     */
    public Membership setId(Integer id) {
        storage.set(DATABASE_ID, id);
        return this;
    }

    @Override
	public Integer getId() {
		return storage.get(DATABASE_ID);
	}

	public Project getProject() {
		return storage.get(PROJECT);
	}

	public Membership setProject(Project project) {
		storage.set(PROJECT, project);
		return this;
	}

	public Integer getUserId() {
		return storage.get(USER_ID);
	}

	public Membership setUserId(Integer id) {
		storage.set(USER_ID, id);
		return this;
	}

    public Integer getGroupId() {
        return storage.get(GROUP_ID);
    }

    public void setGroupId(Integer id) {
		storage.set(GROUP_ID, id);
    }
    
	public String getUserName() {
		return storage.get(USER_NAME);
	}

	public void setUserName(String id) {
		storage.set(USER_NAME, id);
	}

    public String getGroupName() {
        return storage.get(GROUP_NAME);
    }

    public void setGroupName(String id) {
		storage.set(GROUP_NAME, id);
    }

    public Collection<Role> getRoles() {
		return Collections.unmodifiableCollection(storage.get(ROLES));
	}

	public Membership addRoles(Collection<Role> roles) {
		storage.get(ROLES).addAll(roles);
		return this;
	}

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Membership that = (Membership) o;

        if (getId() != null ? !getId().equals(that.getId()) : that.getId() != null) return false;

        return true;
    }

    @Override
    public int hashCode() {
        return getId() != null ? getId().hashCode() : 0;
    }

    @Override
	public String toString() {
		return "Membership [id=" + getId() + ", project=" + getProject() + ", userId="
				+ getUserId() + ", groupId=" + getGroupId() + ", roles=" + getRoles() + "]";
	}

	public PropertyStorage getStorage() {
		return storage;
	}

	/**
	 * Required attributes: 1) project    2) either userId/groupId or roles collection
	 */
	public Membership create() throws RedmineException {
		if (getProject() == null) {
			throw new IllegalArgumentException("Project must be set");
		}
		if (getUserId() == null && getRoles().isEmpty()) {
			throw new IllegalArgumentException("Either User or Roles field must be set");
		}
		return transport.addChildEntry(Project.class, getProject().getId() + "", this);
	}

	public void update() throws RedmineException {
		transport.updateObject(this);
	}

	/**
	 * this object must have ID property set
	 */
	public void delete() throws RedmineException {
		transport.deleteObject(Membership.class, Integer.toString(getId()));
	}

	@Override
	public void setTransport(Transport transport) {
		this.transport = transport;
		PropertyStorageUtil.updateCollections(storage, transport);
	}
} public class News implements Identifiable, Serializable {
    private static final long serialVersionUID = 1L;

    private Integer id;
    private Project project;
    private User user;
    private String title;
    private String description;
    private Date createdOn;
    private String link;

    public News setId(Integer id) {
        this.id = id;
        return this;
    }

    public Integer getId() {
        return id;
    }

    public Project getProject() {
        return project;
    }

    public void setProject(Project aProject) {
        this.project = aProject;
    }

    public User getUser() {
        return user;
    }

    public void setUser(User aUser) {
        this.user = aUser;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String aTitle) {
        this.title = aTitle;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String aDescription) {
        this.description = aDescription;
    }

    public Date getCreatedOn() {
        return createdOn;
    }

    public void setCreatedOn(Date aCreated) {
        this.createdOn = aCreated;
    }

    public String getLink() {
        return link;
    }

    public void setLink(String aLink) {
        this.link = aLink;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        News news = (News) o;

        if (id != null ? !id.equals(news.id) : news.id != null) return false;

        return true;
    }

    @Override
    public int hashCode() {
        return id != null ? id.hashCode() : 0;
    }

    @Override
    public String toString() {
        return "News [id=" + id + ", title=" + title + "]";
    }
} public class Project implements Identifiable, Serializable, FluentStyle {
	private static final long serialVersionUID = 4529305420978716446L;

    private final PropertyStorage storage = new PropertyStorage();

    /**
     * database ID
     */
    public final static Property<Integer> DATABASE_ID = new Property<>(Integer.class, "id");

    /**
     * String "identifier" (human-readable name without spaces and other extra stuff)
     */
    public final static Property<String> STRING_IDENTIFIER = new Property<>(String.class, "identifier");

    /**
     * Can contain any symbols
     */
    public final static Property<String> NAME = new Property<>(String.class, "name");
    public final static Property<String> DESCRIPTION = new Property<>(String.class, "description");
    public final static Property<String> HOMEPAGE = new Property<>(String.class, "homepage");
    public final static Property<Date> CREATED_ON = new Property<>(Date.class, "createdOn");
    public final static Property<Date> UPDATED_ON = new Property<>(Date.class, "updatedOn");

    /**
     * This is the *database ID*, not a String-based key.
     */
    public final static Property<Integer> PARENT_DATABASE_ID = new Property<>(Integer.class, "parentId");
    public final static Property<Integer> STATUS = new Property<>(Integer.class, "status");
    public final static Property<Boolean> PUBLIC = new Property<>(Boolean.class, "public");
    public final static Property<Boolean> INHERIT_MEMBERS = new Property<>(Boolean.class, "inheritMembers");
    public final static Property<Set<CustomField>> CUSTOM_FIELDS = (Property<Set<CustomField>>) new Property(Set.class, "customFields");

    /**
     * Trackers available for this project
     */
    public final static Property<Set<Tracker>> TRACKERS = (Property<Set<Tracker>>) new Property(Set.class, "trackers");
    private Transport transport;

    public Project(Transport transport) {
        this.transport = transport;
        storage.set(CUSTOM_FIELDS, new HashSet<>());
    }

    public Project(Transport transport, String name, String key) {
        this(transport);
        setName(name);
        setIdentifier(key);
    }

    public Project setId(Integer id) {
        storage.set(DATABASE_ID, id);
        return this;
    }

    public String getHomepage() {
        return storage.get(HOMEPAGE);
    }

    public Project setHomepage(String homepage) {
        storage.set(HOMEPAGE, homepage);
        return this;
    }

    /**
     * @return project's string "key" (not a numeric database id!). Example: "project_ABC"
     */
    public String getIdentifier() {
        return storage.get(STRING_IDENTIFIER);
    }

    public Project setIdentifier(String identifier) {
        storage.set(STRING_IDENTIFIER, identifier);
        return this;
    }

    /**
     * @return numeric database ID
     */
    @Override
    public Integer getId() {
        return storage.get(DATABASE_ID);
    }

    /**
     * @return project name
     */
    public String getName() {
        return storage.get(NAME);
    }

    /**
     * @param name the project name
     */
    public Project setName(String name) {
        storage.set(NAME, name);
        return this;
    }

    /**
     * @return Trackers allowed in this project (e.g.: Bug, Feature, Support, Task, ...)
     */
    public Collection<Tracker> getTrackers() {
    	if (!storage.isPropertySet(TRACKERS)) //checks because trackers storage is not created for new projects
    		return Collections.unmodifiableCollection(new HashSet<Tracker>());
        return Collections.unmodifiableCollection(storage.get(TRACKERS));
    }

    /**
     * Adds the specified trackers to this project.
     * If this project is created or updated on the redmine server, 
     * each tracker id must be a valid tracker on the server.
     */
    public Project addTrackers(Collection<Tracker> trackers) {
    	if (!storage.isPropertySet(TRACKERS)) //checks because trackers storage is not created for new projects
    		storage.set(TRACKERS, new HashSet<>());
        storage.get(TRACKERS).addAll(trackers);
        return this;
    }

    /**
     * Removes all of the trackers from this project.
     */
    public Project clearTrackers() {
    	storage.set(TRACKERS, new HashSet<>());
    	return this;
    }

    public Tracker getTrackerByName(String trackerName) {
        for (Tracker t : getTrackers()) {
            if (t.getName().equals(trackerName)) return t;
        }
        return null;
    }

    @Override
    public String toString() {
        return "Project{" +
                "id=" + getId() +
                ", identifier='" + getIdentifier() + '\'' +
                ", name='" + getName() + '\'' +
                '}';
    }

    public String getDescription() {
        return storage.get(DESCRIPTION);
    }

    public Project setDescription(String description) {
        storage.set(DESCRIPTION, description);
        return this;
    }

    public Date getCreatedOn() {
        return storage.get(CREATED_ON);
    }

    public Project setCreatedOn(Date createdOn) {
        storage.set(CREATED_ON, createdOn);
        return this;
    }

    public Date getUpdatedOn() {
        return storage.get(UPDATED_ON);
    }

    public Project setUpdatedOn(Date updatedOn) {
        storage.set(UPDATED_ON, updatedOn);
        return this;
    }

    /**
     * Redmine's REST API "get project" operation does NOT return the 
     * parent project ID in redmine 1.1.2 (and maybe earlier). Which means 
     * calling getParentId() of the project loaded from Redmine server will
     * return <strong>NULL</strong> with that redmine. This bug was fixed in redmine 1.2.1.
     * See bug http://www.redmine.org/issues/8229
     * 
     *
     * @return the parent project Id if it was set programmatically or NULL (!!!) if the project was loaded from the server.
     */
    public Integer getParentId() {
        return storage.get(PARENT_DATABASE_ID);
    }

    public Project setParentId(Integer parentId) {
        storage.set(PARENT_DATABASE_ID, parentId);
        return this;
    }

    /**
     * Returns the project status. This number can theoretically be different for different Redmine versions,
     * But the **current Redmine version in 2018** defines these numbers as:
     * <ul>
     *   <li>1: status active</li>
     *   <li>5: status closed</li>
     *   <li>9: status archived</li>
     * </ul>
     * 
     * <p>see http://www.redmine.org/projects/redmine/repository/entry/trunk/app/models/project.rb
     *
     * @return possibly Redmine-version-specific number that represents project status (active/closed/archived)
     * @since Redmine REST API 2.5.0
     */
    public Integer getStatus() {
    	return storage.get(STATUS);
    }
    /**
     * Sets the project status (Note that this will not take effect when updating a project as
     * the **current Redmine version in 2018** does not allow reopen, close or archive projects,
     * see https://www.redmine.org/issues/13725)
     */
    public Project setStatus(Integer status) {
    	storage.set(STATUS, status);
    	return this;
    }

    /**
     *
     * @return true if the project is public, false if the project is private. 
     * Returns <code>null</code> if the project visibility was not specified or if the project was just retrieved from server.
     *
     * @since Redmine 2.6.0. see http://www.redmine.org/issues/17628 . this property is for writing only before Redmine 2.6.0.
     * The value is not returned by older Redmine versions.
     */
    @Deprecated
    public Boolean getProjectPublic() {
        return storage.get(PUBLIC);
    }
    
    public Project setInheritMembers(Boolean inheritMembers) {
        storage.set(INHERIT_MEMBERS, inheritMembers);
        return this;
    }

    public Boolean getInheritMembers() {
        return storage.get(INHERIT_MEMBERS);
    }

    public Project setProjectPublic(Boolean projectPublic) {
        storage.set(PUBLIC, projectPublic);
        return this;
    }

    public Collection<CustomField> getCustomFields() {
        return storage.get(CUSTOM_FIELDS);
    }

    public Project addCustomFields(Collection<CustomField> customFields) {
        storage.get(CUSTOM_FIELDS).addAll(customFields);
        return this;
    }

    public CustomField getCustomFieldById(int customFieldId) {
        for (CustomField customField : getCustomFields()) {
            if (customFieldId == customField.getId()) {
                return customField;
            }
        }
        return null;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Project project = (Project) o;

        if (getId() != null ? !getId().equals(project.getId()) : project.getId() != null) return false;

        return true;
    }

    @Override
    public int hashCode() {
        return getId() != null ? getId().hashCode() : 0;
    }

    public PropertyStorage getStorage() {
        return storage;
    }

    @Override
    public void setTransport(Transport transport) {
        this.transport = transport;
        PropertyStorageUtil.updateCollections(storage, transport);
    }

    /**
     * Sample usage:
     *
     * <pre>
     * {@code
     * 	Project project = new Project(transport);
     * 	Long timeStamp = Calendar.getInstance().getTimeInMillis();
     * 	String key = "projkey" + timeStamp;
     * 	String name = &quot;project number &quot; + timeStamp;
     * 	String description = &quot;some description for the project&quot;;
     * 	project.setIdentifier(key)
     * 	  .setName(name)
     * 	  .setDescription(description)
     * 	  .create();
     * }
     * </pre>
     * <p> 
     * Note: if {@code project} trackers have not been set with {@link Project#addTrackers} 
     * and if they have been cleared with {@link Project#clearTrackers},
     * the created project will get the server default trackers (if any).
     * Otherwise, the {@code Project} trackers will override the server default settings. 
     * </p>
     *
     * @return the newly created Project object.
     * @throws RedmineAuthenticationException invalid or no API access key is used with the server, which
     *                                 requires authorization. Check the constructor arguments.
     * @throws RedmineException
     */
    public Project create() throws RedmineException {
        return transport.addObject(this, new RequestParam("include",
                "trackers"));
    }

    public void update() throws RedmineException {
        transport.updateObject(this);
    }

    /**
     * The project object must have projectKey set (String-based identifier like "project-ABC",
     * NOT a database numeric ID!)
     *
     * @throws RedmineAuthenticationException invalid or no API access key is used with the server, which
     *                                 requires authorization. Check the constructor arguments.
     * @throws NotFoundException       if the project with this key is not found
     */
    public void delete() throws RedmineException {
        transport.deleteObject(Project.class, getIdentifier());
    }
} public class Role {

    /**
     * database ID.
     */
    private Integer id;
	private String name;
	private Boolean inherited;

	private final Collection<String> permissions = new HashSet<>();

    /**
     * @param id database ID.
     */
    public Role setId(Integer id) {
        this.id = id;
        return this;
    }

    public Integer getId() {
		return id;
	}

	public String getName() {
		return name;
	}

	public Role setName(String name) {
		this.name = name;
		return this;
	}

	public Boolean getInherited() {
		return inherited;
	}

	public void setInherited(Boolean inherited) {
		this.inherited = inherited;
	}
	
	public Collection<String> getPermissions() {
        return Collections.unmodifiableCollection(permissions);
    }

    public void addPermissions(Collection<String> permissions) {
        this.permissions.addAll(permissions);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Role role = (Role) o;

        if (id != null ? !id.equals(role.id) : role.id != null) return false;

        return true;
    }

    @Override
    public int hashCode() {
        return id != null ? id.hashCode() : 0;
    }

    @Override
	public String toString() {
		return "Role [id=" + id + ", name=" + name + ", inherited=" + inherited
				+ "]";
	}
} public class SavedQuery {
    private Integer id;

    private String name;
    private boolean publicQuery;
    private Integer projectId;

    public SavedQuery setId(Integer id) {
        this.id = id;
        return this;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        SavedQuery that = (SavedQuery) o;

        if (id != null ? !id.equals(that.id) : that.id != null) return false;

        return true;
    }

    @Override
    public int hashCode() {
        return id != null ? id.hashCode() : 0;
    }

    public Integer getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public SavedQuery setName(String name) {
        this.name = name;
        return this;
    }

    public boolean isPublicQuery() {
        return publicQuery;
    }

    public SavedQuery setPublicQuery(boolean isPublic) {
        this.publicQuery = isPublic;
        return this;
    }

    public Integer getProjectId() {
        return projectId;
    }

    public SavedQuery setProjectId(Integer value) {
        this.projectId = value;
        return this;
    }

    @Override
    public String toString() {
        return "SavedQuery [id=" + id + ", name=" + name + ", publicQuery="
                + publicQuery + ", projectId=" + projectId + "]";
    }

} public class TimeEntry implements Identifiable, FluentStyle {

    private final PropertyStorage storage = new PropertyStorage();

    /**
     * database numeric Id
     */
    public final static Property<Integer> DATABASE_ID = new Property<>(Integer.class, "id");

    /**
     * database Id of the Issue
     */
    public final static Property<Integer> ISSUE_ID = new Property<>(Integer.class, "issueId");

    /**
     * database Id of the project
     */
    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<String> USER_NAME = new Property<>(String.class, "userName");
    public final static Property<Integer> USER_ID = new Property<>(Integer.class, "userId");
    public final static Property<String> ACTIVITY_NAME = new Property<>(String.class, "activityName");
    public final static Property<Integer> ACTIVITY_ID = new Property<>(Integer.class, "activityId");
    public final static Property<Float> HOURS = new Property<>(Float.class, "hours");
    public final static Property<String> COMMENT = new Property<>(String.class, "comment");
    public final static Property<Date> SPENT_ON = new Property<>(Date.class, "spentOn");
    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 static final Property<Set<CustomField>> CUSTOM_FIELDS = new Property(Set.class, "custom_fields");
    private Transport transport;

    public TimeEntry(Transport transport) {
        setTransport(transport);
        storage.set(CUSTOM_FIELDS, new HashSet<>());
    }

    /**
     * @param id database Id
     */
    public TimeEntry setId(Integer id) {
        storage.set(DATABASE_ID, id);
        return this;
    }

    public Integer getId() {
        return storage.get(DATABASE_ID);
    }

    public Integer getUserId() {
        return storage.get(USER_ID);
    }

    public TimeEntry setUserId(Integer userId) {
        storage.set(USER_ID, userId);
        return this;
    }

    public Integer getIssueId() {
        return storage.get(ISSUE_ID);
    }

    public TimeEntry setIssueId(Integer issueId) {
        storage.set(ISSUE_ID, issueId);
        return this;
    }

    public Integer getProjectId() {
        return storage.get(PROJECT_ID);
    }

    public TimeEntry setProjectId(Integer projectId) {
        storage.set(PROJECT_ID, projectId);
        return this;
    }

    public String getProjectName() {
        return storage.get(PROJECT_NAME);
    }

    public void setProjectName(String projectName) {
        storage.set(PROJECT_NAME, projectName);
    }

    public String getActivityName() {
        return storage.get(ACTIVITY_NAME);
    }

    public void setActivityName(String activityName) {
        storage.set(ACTIVITY_NAME, activityName);
    }

    public Integer getActivityId() {
        return storage.get(ACTIVITY_ID);
    }

    public TimeEntry setActivityId(Integer activityId) {
        storage.set(ACTIVITY_ID, activityId);
        return this;
    }

    public Float getHours() {
        return storage.get(HOURS);
    }

    public TimeEntry setHours(Float hours) {
        storage.set(HOURS, hours);
        return this;
    }

    public String getComment() {
        return storage.get(COMMENT);
    }

    public TimeEntry setComment(String comment) {
        storage.set(COMMENT, comment);
        return this;
    }

    public Date getSpentOn() {
        return storage.get(SPENT_ON);
    }

    public TimeEntry setSpentOn(Date spentOn) {
        storage.set(SPENT_ON, spentOn);
        return this;
    }

    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 String getUserName() {
        return storage.get(USER_NAME);
    }

    public void setUserName(String userName) {
        storage.set(USER_NAME, userName);
    }

    /**
     * @return the custom field with given name or NULL if the field is not found
     */
    public CustomField getCustomField(String name) {
        return storage.get(CUSTOM_FIELDS).stream().filter(a -> a.getName().equals(name)).findFirst().orElse(null);
    }

    @Override
    public String toString() {
        return "User \"" + getUserName() + "\" spent " + getHours()
                + " hours on task " + getIssueId() + " (project \"" + getProjectName()
                + "\") doing " + getActivityName();
    }

    public Set<CustomField> getCustomFields() {
        return storage.get(CUSTOM_FIELDS);
    }

    public void clearCustomFields() {
        storage.set(CUSTOM_FIELDS, new HashSet<>());
    }

    public void addCustomFields(Collection<CustomField> customFields) {
        storage.get(CUSTOM_FIELDS).addAll(customFields);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        TimeEntry timeEntry = (TimeEntry) o;

        if (getId() != null ? !getId().equals(timeEntry.getId()) : timeEntry.getId() != null) return false;

        return true;
    }

    @Override
    public int hashCode() {
        return getId() != null ? getId().hashCode() : 0;
    }

    public boolean isValid() {
        return (getHours() != null) && (getProjectId() != null || getIssueId() != null);
    }

    public PropertyStorage getStorage() {
        return storage;
    }

    @Override
    public void setTransport(Transport transport) {
        this.transport = transport;
        PropertyStorageUtil.updateCollections(storage, transport);
    }

    public TimeEntry create() throws RedmineException {
        validate(this);
        return transport.addObject(this);
    }


    public void update() throws RedmineException {
        validate(this);
        transport.updateObject(this);
    }


    public void delete() throws RedmineException {
        transport.deleteObject(TimeEntry.class, Integer.toString(getId()));
    }

    private static void validate(TimeEntry obj) {
        if (!obj.isValid()) {
            throw new IllegalArgumentException("You have to either define a Project or Issue ID for a Time Entry. "
                    + "The given Time Entry object has neither defined.");
        }
    }
} public class TimeEntryActivity {

    /**
     * database Id
     */
    private Integer id;

    private String name;
    private boolean isDefault;

    /**
     * @param id database Id
     */
    public TimeEntryActivity setId(Integer id) {
        this.id = id;
        return this;
    }

    public Integer getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public TimeEntryActivity setName(String name) {
        this.name = name;
        return this;
    }

    public boolean isDefault() {
        return isDefault;
    }

    public TimeEntryActivity setDefault(boolean isDefault) {
        this.isDefault = isDefault;
        return this;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        TimeEntryActivity that = (TimeEntryActivity) o;

        if (id != null ? !id.equals(that.id) : that.id != null) return false;

        return true;
    }

    @Override
    public int hashCode() {
        return id != null ? id.hashCode() : 0;
    }

    @Override
    public String toString() {
        return "TimeEntryActivity [id=" + id + ", name=" + name + ", isDefault="
                + isDefault + "]";
    }

} public class Tracker implements Identifiable, Serializable {

    /**
     * database ID.
     */
    private Integer id;
    private String name;

    /**
     * @param id database ID.
     */
    public Tracker setId(Integer id) {
        this.id = id;
        return this;
    }

    public Integer getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public Tracker setName(String name) {
        this.name = name;
        return this;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Tracker tracker = (Tracker) o;

        if (id != null ? !id.equals(tracker.id) : tracker.id != null) return false;

        return true;
    }

    @Override
    public int hashCode() {
        return id != null ? id.hashCode() : 0;
    }
} public class User implements Identifiable, FluentStyle {

    public static final Integer STATUS_ANONYMOUS = 0;

    public static final Integer STATUS_ACTIVE = 1;

    public static final Integer STATUS_REGISTERED = 2;

    public static final Integer STATUS_LOCKED = 3;

    private final PropertyStorage storage = new PropertyStorage();

    public final static Property<Integer> ID = new Property<>(Integer.class, "id");
    public final static Property<String> LOGIN = new Property<>(String.class, "login");
    public final static Property<String> PASSWORD = new Property<>(String.class, "password");
    public final static Property<String> FIRST_NAME = new Property<>(String.class, "firstName");
    public final static Property<String> LAST_NAME = new Property<>(String.class, "lastName");
    public final static Property<String> MAIL = new Property<>(String.class, "mail");
    public final static Property<String> API_KEY = new Property<>(String.class, "apiKey");
    public final static Property<Date> CREATED_ON = new Property<>(Date.class, "createdOn");
    public final static Property<Date> LAST_LOGIN_ON = new Property<>(Date.class, "lastLoginOn");
    public final static Property<Integer> AUTH_SOURCE_ID = new Property<>(Integer.class, "authSourceId");
    public final static Property<Integer> STATUS = new Property<>(Integer.class, "status");
    public final static Property<String> MAIL_NOTIFICATION = new Property<>(String.class, "mail_notification");
    public final static Property<Boolean> MUST_CHANGE_PASSWD = new Property<>(Boolean.class, "must_change_passwd");
    public final static Property<Boolean> GENERATE_PASSWORD = new Property<>(Boolean.class, "generate_password");

    public final static Property<Set<CustomField>> CUSTOM_FIELDS = (Property<Set<CustomField>>) new Property(Set.class, "customFields");
    public final static Property<Set<Membership>> MEMBERSHIP = (Property<Set<Membership>>) new Property(Set.class, "membership");
    public final static Property<Set<Group>> GROUPS = (Property<Set<Group>>) new Property(Set.class, "groups");

    private Transport transport;

    public User(Transport transport) {
        initCollections();
        setTransport(transport);
    }

    private void initCollections() {
        this.storage.set(CUSTOM_FIELDS, new HashSet<>());
        this.storage.set(MEMBERSHIP, new HashSet<>());
        this.storage.set(GROUPS, new HashSet<>());
    }

    public User setId(int id) {
        storage.set(ID, id);
        return this;
    }

    @Override
    public Integer getId() {
        return storage.get(ID);
    }

    @Override
    public String toString() {
        return getLogin();
    }

    public String getLogin() {
        return storage.get(LOGIN);
    }

    public User setLogin(String login) {
        storage.set(LOGIN, login);
        return this;
    }

    public String getFirstName() {
        return storage.get(FIRST_NAME);
    }

    public User setFirstName(String firstName) {
        storage.set(FIRST_NAME, firstName);
        return this;
    }

    public String getLastName() {
        return storage.get(LAST_NAME);
    }

    public User setLastName(String lastName) {
        storage.set(LAST_NAME, lastName);
        return this;
    }

    /**
     * This field is empty when using issues.get(i).getAssignee().getMail()
     */
    public String getMail() {
        return storage.get(MAIL);
    }

    public User setMail(String mail) {
        storage.set(MAIL, mail);
        return this;
    }

    public Date getCreatedOn() {
        return storage.get(CREATED_ON);
    }

    public User setCreatedOn(Date createdOn) {
        storage.set(CREATED_ON, createdOn);
        return this;
    }

    public Date getLastLoginOn() {
        return storage.get(LAST_LOGIN_ON);
    }

    public User setLastLoginOn(Date lastLoginOn) {
        storage.set(LAST_LOGIN_ON, lastLoginOn);
        return this;
    }

    public String getApiKey() {
        return storage.get(API_KEY);
    }

    /**
     * APIKey property is read-only. This setter is only for serialization JSon.
     * The value you set using this method will be ignored by the server.
     */
    @Deprecated
    public User setApiKey(String apiKey) {
        storage.set(API_KEY, apiKey);
        return this;
    }

    /**
     * This value is not returned by redmine on existing users.
     * 
     * @return - The Authentication Source ID, if you set it on your own.
     */
    @Deprecated
    public Integer getAuthSourceId() {
		return storage.get(AUTH_SOURCE_ID);
	}

	public User setAuthSourceId(Integer authSource) {
        storage.set(AUTH_SOURCE_ID, authSource);
        return this;
	}

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        User user = (User) o;

        return getId() != null ? getId().equals(user.getId()) : user.getId() == null;
    }

    @Override
    public int hashCode() {
        return getId() != null ? getId().hashCode() : 0;
    }

    /**
     * @return `firstName` + space + `lastName` if `lastName` is not null. otherwise returns value of `firstName`
     */
    public String getFullName() {
        String lastNameOrEmpty = getLastName() == null ? "" : " " + getLastName();
        return getFirstName() + lastNameOrEmpty;
    }

    /**
     * This is a BIG HACK just to workaround Redmine REST API limitation.
     * see http://www.redmine.org/issues/7487
     */
    public User setFullName(String fullName) {
        int ind = fullName.indexOf(' ');
        if (ind != -1) {
            setFirstName(fullName.substring(0, ind));
            setLastName(fullName.substring(ind + 1));
        } else {
            setFirstName(fullName);
        }
        return this;
    }

    public String getPassword() {
        return storage.get(PASSWORD);
    }

    public User setPassword(String password) {
        storage.set(PASSWORD, password);
        return this;
    }

    /**
     * @return the value or NULL if the field is not found
     */
    public String getCustomField(String fieldName) {
        for (CustomField f : getCustomFields()) {
            if (f.getName().equals(fieldName)) {
                return f.getValue();
            }
        }
        return null;
    }

    /**
     * @return Custom Fields, 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.
     * ID can be seen in database or in Redmine administration when editing the custom field (number is part of the URL!).
     */
    public User addCustomFields(Collection<CustomField> customFields) {
        storage.get(CUSTOM_FIELDS).addAll(customFields);
        return this;
    }

    /**
     * If there is a custom field with the same ID already present,
     * the new field replaces the old one.
     *
     * @param customField the field to add.
     */
    public User addCustomField(CustomField customField) {
        storage.get(CUSTOM_FIELDS).add(customField);
        return this;
    }

	public Collection<Membership> getMemberships() {
		return Collections.unmodifiableCollection(storage.get(MEMBERSHIP));
	}

	public User addMemberships(Collection<Membership> memberships) {
		storage.get(MEMBERSHIP).addAll(memberships);
        return this;
	}

	public Collection<Group> getGroups() {
        return Collections.unmodifiableCollection(storage.get(GROUPS));
    }

	public User addGroups(Collection<Group> groups) {
        if (!storage.isPropertySet(GROUPS)) {
            storage.set(GROUPS, new HashSet<Group>());
        }
        storage.get(GROUPS).addAll(groups);
        return this;
	}

    /**
     * This field is visible to Admin only.
     * <p>
     * Returns the user status. This number can theoretically be different for different Redmine versions,
     * But the **current Redmine version in 2018** defines these numbers as:
     * <ul>
     *   <li>0: status anonymous</li>
     *   <li>1: status active</li>
     *   <li>2: status registered</li>
     *   <li>3: status locked</li>
     * </ul>
     *
     * <p>see http://www.redmine.org/projects/redmine/repository/entry/trunk/app/models/principal.rb#L22-25
     * 
     * @return possibly Redmine-version-specific number that represents user status (active/locked/etc)
     * @since Redmine REST API 2.4.0
     */
    public Integer getStatus() {
        return storage.get(STATUS);
    }

    /**
     * Sets the user status.
     * 
     * @param status {@link #STATUS_ACTIVE}, {@link #STATUS_LOCKED}, etc...
     */
    public User setStatus(Integer status) {
        storage.set(STATUS, status);
        return this;
    }

    public User setMailNotification(String mailNotification) {
        storage.set(MAIL_NOTIFICATION, mailNotification);
        return this;
    }

    public User setMustChangePasswd(Boolean mustChangePasswd) {
        storage.set(MUST_CHANGE_PASSWD, mustChangePasswd);
        return this;
    }

    public User setGeneratePassword(Boolean generatePassword) {
        storage.set(GENERATE_PASSWORD, generatePassword);
        return this;
    }

    public PropertyStorage getStorage() {
        return storage;
    }

    @Override
    public void setTransport(Transport transport) {
        this.transport = transport;
        PropertyStorageUtil.updateCollections(storage, transport);
    }

    public User create() throws RedmineException {
        return transport.addObject(this);
    }

    public void update() throws RedmineException {
        transport.updateObject(this);
    }

    /**
     * Adds this user (ID must be set!) to the given group.
     * <p>
     * Note: "add to group" operation used to be safe (idempotent) for Redmine 2.6.x, but FAILS for Redmine 3.0.0 when
     * executed twice on the same user. I submitted a bug: http://www.redmine.org/issues/19363 which was closed as
     * "invalid"...
     *
     * @param groupId - id of the group to add to.
     * @throws RedmineException
     * @since Redmine 2.1
     */
    public void addToGroup(int groupId) throws RedmineException {
        transport.addUserToGroup(getId(), groupId);
    }

    /**
     * The user object must have ID set.
     */
    public void delete() throws RedmineException {
        transport.deleteObject(User.class, Integer.toString(getId()));
    }
} public class Version implements Identifiable, FluentStyle {
    /*
      * <id>1</id> <project name="Redmine" id="1"/> <name>0.7</name>
      * <description/> <status>closed</status> <due_date>2008-04-28</due_date>
      * <sharing>descendants</sharing>
      * <created_on>2008-03-09T12:52:06+01:00</created_on>
      * <updated_on>2009-11-15T12:22:12+01:00</updated_on>
      */

    public static final String STATUS_OPEN = "open";
    public static final String STATUS_LOCKED = "locked";
    public static final String STATUS_CLOSED = "closed";

    public static final String SHARING_NONE = "none";
    public static final String SHARING_DESCENDANTS = "descendants";
    public static final String SHARING_HIERARCHY = "hierarchy";
    public static final String SHARING_TREE = "tree";
    public static final String SHARING_SYSTEM = "system";

    private final PropertyStorage storage = new PropertyStorage();

    /**
     * database numeric Id
     */
    public final static Property<Integer> DATABASE_ID = new Property<>(Integer.class, "id");

    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<String> NAME = new Property<>(String.class, "name");
    public final static Property<String> DESCRIPTION = new Property<>(String.class, "description");
    public final static Property<String> STATUS = new Property<>(String.class, "status");
    public final static Property<String> SHARING = new Property<>(String.class, "sharing");
    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<Set<CustomField>> CUSTOM_FIELDS = (Property<Set<CustomField>>) new Property(Set.class, "customFields");
    private Transport transport;

    public Version() {
        initCollections(storage);
    }

    public Version(Transport transport, int projectId, String name) {
        this();
        setTransport(transport);
        setProjectId(projectId);
        setName(name);
    }

    public Version setId(Integer id) {
        storage.set(DATABASE_ID, id);
        return this;
    }

    private void initCollections(PropertyStorage storage) {
        storage.set(CUSTOM_FIELDS, new HashSet<>());
    }

    /**
     * Version objects are considered to be equal if their IDs are not null and equal.
     */
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Version version = (Version) o;

        if (getId() != null ? !getId().equals(version.getId()) : version.getId() != null) return false;

        return true;
    }

    @Override
    public int hashCode() {
        return getId() != null ? getId().hashCode() : 0;
    }

    public Date getCreatedOn() {
        return storage.get(CREATED_ON);
    }

    public String getDescription() {
        return storage.get(DESCRIPTION);
    }

    public Date getDueDate() {
        return storage.get(DUE_DATE);
    }

    @Override
    public Integer getId() {
        return storage.get(DATABASE_ID);
    }

    public Integer getProjectId() {
        return storage.get(PROJECT_ID);
    }

    public Version setProjectId(Integer projectId) {
        storage.set(PROJECT_ID, projectId);
        return this;
    }

    public String getProjectName() {
        return storage.get(PROJECT_NAME);
    }

    public Version setProjectName(String name) {
        storage.set(PROJECT_NAME, name);
        return this;
    }

    public String getName() {
        return storage.get(NAME);
    }

    public String getSharing() {
        return storage.get(SHARING);
    }

    public String getStatus() {
        return storage.get(STATUS);
    }

    public Date getUpdatedOn() {
        return storage.get(UPDATED_ON);
    }

    public Version setCreatedOn(Date createdOn) {
        storage.set(CREATED_ON, createdOn);
        return this;
    }

    public Version setDescription(String description) {
        storage.set(DESCRIPTION, description);
        return this;
    }

    public Version setDueDate(Date dueDate) {
        storage.set(DUE_DATE, dueDate);
        return this;
    }

    public Version setName(String name) {
        storage.set(NAME, name);
        return this;
    }

    public Version setSharing(String sharing) {
        storage.set(SHARING, sharing);
        return this;
    }

    public Version setStatus(String status) {
        storage.set(STATUS, status);
        return this;
    }

    public void setUpdatedOn(Date updatedOn) {
        storage.set(UPDATED_ON, updatedOn);
    }

    public Collection<CustomField> getCustomFields() {
        return Collections.unmodifiableCollection(storage.get(CUSTOM_FIELDS));
    }

    public Version addCustomFields(Collection<CustomField> customFields) {
        storage.get(CUSTOM_FIELDS).addAll(customFields);
        return this;
    }

    /**
     * @return the field with the given ID or NULL if the field is not found.
     */
    public CustomField getCustomFieldById(int customFieldId) {
        for (CustomField customField : getCustomFields()) {
            if (customFieldId == customField.getId()) {
                return customField;
            }
        }
        return null;
    }

    @Override
    public String toString() {
        return "Version [id=" + getId() + ", name=" + getName() + "]";
    }

    public PropertyStorage getStorage() {
        return storage;
    }


    /**
     * creates a new {@link com.taskadapter.redmineapi.bean.Version} for the {@link Project} contained. <br>
     * Pre-condition: attribute projectId must be set.
     *
     * @return the new {@link com.taskadapter.redmineapi.bean.Version} created by Redmine
     * @throws IllegalArgumentException thrown in case the version does not contain a project.
     * @throws RedmineAuthenticationException  thrown in case something went wrong while trying to login
     * @throws RedmineException         thrown in case something went wrong in Redmine
     * @throws NotFoundException        thrown in case an object can not be found
     */
    public Version create() throws RedmineException {
        // check project
        if (getProjectId() == null) {
            throw new IllegalArgumentException(
                    "Version must contain projectId");
        }
        return transport.addChildEntry(Project.class, getProjectId().toString(), this);
    }

    /**
     * update this object on the server
     */
    public void update() throws RedmineException {
        transport.updateObject(this);
    }

    /**
     * deletes this {@link Version} from the referenced {@link Project}. <br>
     *
     * @throws RedmineAuthenticationException thrown in case something went wrong while trying to login
     * @throws RedmineException        thrown in case something went wrong in Redmine
     * @throws NotFoundException       thrown in case an object can not be found
     */
    public void delete() throws RedmineException {
        transport.deleteObject(Version.class, Integer.toString(getId()));
    }

    @Override
    public void setTransport(Transport transport) {
        this.transport = transport;
        PropertyStorageUtil.updateCollections(storage, transport);
    }
} public class Watcher {

    private Integer id;
    private String name;

    /**
     * @param id database Id
     */
    public Watcher setId(Integer id) {
        this.id = id;
        return this;
    }

    public Integer getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Watcher watcher = (Watcher) o;

        if (id != null ? !id.equals(watcher.id) : watcher.id != null) return false;

        return true;
    }

    @Override
    public int hashCode() {
        return id != null ? id.hashCode() : 0;
    }

    @Override
    public String toString() {
        return "Watcher{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }

} public class WikiPage {
    private final PropertyStorage storage = new PropertyStorage();

    public final static Property<String> TITLE = new Property<>(String.class, "title");
    public final static Property<Integer> VERSION = new Property<>(Integer.class, "version");
    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 String getTitle() {
        return storage.get(TITLE);
    }

    public WikiPage setTitle(String title) {
        storage.set(TITLE, title);
        return this;
    }

    public Integer getVersion() {
        return storage.get(VERSION);
    }

    public WikiPage setVersion(Integer version) {
        storage.set(VERSION, version);
        return this;
    }

    public Date getCreatedOn() {
        return storage.get(CREATED_ON);
    }

    public WikiPage setCreatedOn(Date createdOn) {
        storage.set(CREATED_ON, createdOn);
        return this;
    }

    public Date getUpdatedOn() {
        return storage.get(UPDATED_ON);
    }

    public WikiPage setUpdatedOn(Date updatedOn) {
        storage.set(UPDATED_ON, updatedOn);
        return this;
    }

    @Override
    public String toString() {
        return "WikiPage{" +
                "title='" + getTitle() + '\'' +
                ", version=" + getVersion() +
                ", createdOn=" + getCreatedOn() +
                ", updatedOn=" + getUpdatedOn() +
                '}';
    }
} public class WikiPageDetail implements FluentStyle {

    public final static Property<WikiPageDetail> PARENT = new Property<>(WikiPageDetail.class, "parent");
    public final static Property<String> TEXT = new Property<>(String.class, "text");
    public final static Property<String> PROJECT_KEY = new Property<>(String.class, "project_key");
    public final static Property<User> USER = new Property<>(User.class, "user");
    public final static Property<String> COMMENTS = new Property<>(String.class, "comments");
    public final static Property<List<Attachment>> ATTACHMENTS = (Property<List<Attachment>>) new Property(List.class, "uploads");

    private final PropertyStorage storage = new PropertyStorage();
    private final WikiPage wikiPage = new WikiPage();
    private Transport transport;

    public WikiPageDetail(Transport transport) {
        storage.set(ATTACHMENTS, new ArrayList<>());
        setTransport(transport);
    }

    /**
     * @return the comment entered when the wiki page was last edited
     */
    public String getComments() {
        return storage.get(COMMENTS);
    }

    public WikiPageDetail setComments(String comments) {
        storage.set(COMMENTS, comments);
        return this;
    }

    public List<Attachment> getAttachments() {
        return storage.get(ATTACHMENTS);
    }

    public WikiPageDetail setAttachments(List<Attachment> attachments) {
        storage.set(ATTACHMENTS, attachments);
        return this;
    }

    public WikiPageDetail getParent() {
        return storage.get(PARENT);
    }

    public WikiPageDetail setParent(WikiPageDetail parent) {
        storage.set(PARENT, parent);
        return this;
    }

    public String getText() {
        return storage.get(TEXT);
    }

    public WikiPageDetail setText(String text) {
        storage.set(TEXT, text);
        return this;
    }

    public User getUser() {
        return storage.get(USER);
    }

    public Integer getVersion() {
        return wikiPage.getVersion();
    }

    public WikiPageDetail setUser(User user) {
        storage.set(USER, user);
        return this;
    }

    public WikiPageDetail setTitle(String title) {
        wikiPage.setTitle(title);
        return this;
    }

    public WikiPageDetail setVersion(Integer version) {
        wikiPage.setVersion(version);
        return this;
    }

    public WikiPageDetail setCreatedOn(Date createdOn) {
        wikiPage.setCreatedOn(createdOn);
        return this;
    }

    @Override
    public String toString() {
        return "WikiPageDetail{" +
                "text='" + getText() + '\'' +
                '}';
    }

    public PropertyStorage getStorage() {
        return storage;
    }

    public String getTitle() {
        return wikiPage.getTitle();
    }

    public Date getCreatedOn() {
        return wikiPage.getCreatedOn();
    }

    public Date getUpdatedOn() {
        return wikiPage.getUpdatedOn();
    }

    public WikiPageDetail setUpdatedOn(Date updatedOn) {
        wikiPage.setUpdatedOn(updatedOn);
        return this;
    }


    /**
     * projectKey must be set before calling this.
     *
     * At this moment create() simply calls update(). There are no differences between these two functions.
     */
    public void create() throws RedmineException {
        update();
    }

    /**
     * projectKey must be set before calling this.
     * Version must be set to the latest version of the document.
     */
    public void update() throws RedmineException {
        String urlSafeTitle = getUrlSafeString(getTitle());
        transport.updateChildEntry(Project.class, getProjectKey(), this, urlSafeTitle);
    }

    @Override
    public void setTransport(Transport transport) {
        this.transport = transport;
    }

    public static String getUrlSafeString(String string) {
        String urlSafeTitle;
        try {
            urlSafeTitle = URLEncoder.encode(string, StandardCharsets.UTF_8.name());
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("UnsupportedEncodingException when converting the page title to url: " + e.toString(), e);
        }
        return urlSafeTitle;
    }

    public WikiPageDetail setProjectKey(String projectKey) {
        storage.set(PROJECT_KEY, projectKey);
        return this;
    }

    public String getProjectKey() {
        return storage.get(PROJECT_KEY);
    }
} public class BaseCommunicator implements Communicator<HttpResponse> {
	private final Logger logger = LoggerFactory.getLogger(BaseCommunicator.class);

	private final HttpClient client;

    public BaseCommunicator(HttpClient client) {
        this.client = client;
    }

	// TODO lots of usages process 404 code themselves, but some don't.
	// check if we can process 404 code in this method instead of forcing
	// clients to deal with it.

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.taskadapter.redmineapi.internal.comm.Communicator#sendRequest(org.apache.http
	 * .HttpRequest)
	 */
	@Override
	public <R> R sendRequest(HttpRequest request,
			ContentHandler<HttpResponse, R> handler) throws RedmineException {
		logger.debug(request.getRequestLine().toString());

		request.addHeader("Accept-Encoding", "gzip");
		try {
			final HttpResponse httpResponse = client
					.execute((HttpUriRequest) request);
			try {
				return handler.processContent(httpResponse);
			} finally {
				EntityUtils.consume(httpResponse.getEntity());
			}
		} catch (ClientProtocolException e1) {
			throw new RedmineFormatException(e1);
		} catch (IOException e1) {
			throw new RedmineTransportException("Cannot fetch data from "
					+ getMessageURI(request) + " : "
							+ e1.toString(), e1);
		}
	}

	private String getMessageURI(HttpRequest request) {
		final String uri = request.getRequestLine().getUri();
		final int paramsIndex = uri.indexOf('?');
		if (paramsIndex >= 0)
			return uri.substring(0, paramsIndex);
		return uri;
	}
} public final class BasicHttpResponse {
	private final int responseCode;
	private final InputStream stream;
	private final String charset;

	public BasicHttpResponse(int responseCode, InputStream stream,
			String charset) {
		super();
		this.responseCode = responseCode;
		this.stream = stream;
		this.charset = charset;
	}

	public int getResponseCode() {
		return responseCode;
	}

	public InputStream getStream() {
		return stream;
	}

	public String getCharset() {
		return charset;
	}

} public interface Communicator<K> {

	/**
	 * Performs a request.
	 * 
	 * @return the response body.
	 */
	<R> R sendRequest(HttpRequest request, ContentHandler<K, R> contentHandler) throws RedmineException;

} public final class Communicators {
	private static final ContentHandler<Object, Object> IDENTITY_HANDLER = new ContentHandler<Object, Object>() {
		@Override
		public Object processContent(Object content) throws RedmineException {
			return content;
		}
	};

	private static final ContentHandler<HttpResponse, BasicHttpResponse> TRANSPORT_DECODER = new TransportDecoder();

	private static final ContentHandler<BasicHttpResponse, Reader> CHARACTER_DECODER = new ContentHandler<BasicHttpResponse, Reader>() {
		@Override
		public Reader processContent(BasicHttpResponse content) throws RedmineException {
			final String charset = content.getCharset();
			try {
				return new InputStreamReader(content.getStream(), charset);
			} catch (UnsupportedEncodingException e) {
				throw new RedmineTransportException(
						"Unsupported response charset " + charset, e);
			}
		}
	};
	
	private static final ContentHandler<BasicHttpResponse, String> CHAR_CONTENT_READER = compose(
			Communicators::readAll, CHARACTER_DECODER);

	static String readAll(Reader r) throws RedmineException {
		final StringWriter writer = new StringWriter();
		final char[] buffer = new char[4096];
		int readed;
		try {
			while ((readed = r.read(buffer)) > 0) {
				writer.write(buffer, 0, readed);
			}
			r.close();
			writer.close();
			return writer.toString();
		} catch (IOException e) {
			throw new RedmineTransportException(e);
		}
	}

	public static <K, V> SimpleCommunicator<V> simplify(
			Communicator<K> communicator, ContentHandler<K, V> handler) {
		return new BasicSimplifier<>(handler, communicator);
	}

	@SuppressWarnings("unchecked")
	public static <K> ContentHandler<K, K> identityHandler() {
		return (ContentHandler<K, K>) IDENTITY_HANDLER;
	}

	public static ContentHandler<BasicHttpResponse, String> contentReader() {
		return CHAR_CONTENT_READER;
	}

	public static <K, I, R> ContentHandler<K, R> compose(
			ContentHandler<I, R> cont1, ContentHandler<K, I> cont2) {
		return new ComposingHandler<>(cont1, cont2);
	}

	public static <K, R> Communicator<R> fmap(Communicator<K> comm,
			ContentHandler<K, R> handler) {
		return new FmapCommunicator<>(handler, comm);
	}

	public static ContentHandler<HttpResponse, BasicHttpResponse> transportDecoder() {
		return TRANSPORT_DECODER;
	}
} public interface ContentHandler<K, R> {
	/**
	 * Consumes content of a specified type and returns a specified result.
	 * 
	 * @param content
	 *            content to process.
	 * @return processed content.
	 * @throws RedmineException
	 *             if something goes wrong.
	 */
	R processContent(K content) throws RedmineException;
} public class RedmineAuthenticator<K> implements Communicator<K> {
	/**
	 * Header value.
	 */
	private String authKey;

	/**
	 * Used charset.
	 */
	private final String charset;

	/**
	 * Peer communicator.
	 */
	private final Communicator<K> peer;

	public RedmineAuthenticator(Communicator<K> peer, String charset) {
		this.peer = peer;
		this.charset = charset;
	}

	public void setCredentials(String login, String password) {
		if (login == null) {
			authKey = null;
			return;
		}
		try {
			authKey = "Basic "
					+ Base64.encodeBase64String(
							(login + ':' + password).getBytes(charset)).trim();
		} catch (UnsupportedEncodingException e) {
			throw new RedmineInternalError(e);
		}
	}

	@Override
	public <R> R sendRequest(HttpRequest request, ContentHandler<K, R> handler)
			throws RedmineException {
		if (authKey != null)
			request.addHeader("Authorization", authKey);
		return peer.sendRequest(request, handler);
	}

} public final class RedmineErrorHandler implements
        ContentHandler<BasicHttpResponse, BasicHttpResponse> {

	private static final Map<String, String> ERROR_REMAP = new HashMap<>();

	static {
		ERROR_REMAP
				.put("Priority can't be blank",
						"Priority can't be blank. No default priority is set in the Redmine server settings. please use menu \"Administration -> Enumerations -> Issue Priorities\" to set the default priority.");
	}

	@Override
	public BasicHttpResponse processContent(BasicHttpResponse httpResponse)
			throws RedmineException {
		final int responseCode = httpResponse.getResponseCode();
		if (responseCode == HttpStatus.SC_UNAUTHORIZED) {
			throw new RedmineAuthenticationException(
					"Authorization error. Please check if you provided a valid API access key or Login and Password and REST API service is enabled on the server.");
		}
		if (responseCode == HttpStatus.SC_FORBIDDEN) {
			throw new NotAuthorizedException(
					"Forbidden. Please check the user has proper permissions.");
		}
		if (responseCode == HttpStatus.SC_NOT_FOUND) {
			throw new NotFoundException(
					"Server returned '404 not found'. response body:"
							+ getContent(httpResponse));
		}

		if (responseCode == HttpStatus.SC_UNPROCESSABLE_ENTITY) {
			try {
				final List<String> sourceErrors = RedmineJSONParser.parseErrors(getContent(httpResponse));
				final List<String> errors = sourceErrors.stream()
						.map(this::remap)
						.collect(Collectors.toList());
				throw new RedmineProcessingException(errors);
			} catch (JSONException e) {
				throw new RedmineFormatException("Bad redmine error response", e);
			}
		}
		return httpResponse;
	}

	private String remap(String message) {
		final String guess = ERROR_REMAP.get(message);
		return guess != null ? guess : message;
	}

	private String getContent(BasicHttpResponse entity) throws RedmineException {
		return Communicators.contentReader().processContent(entity);
	}

} public class JsonInput {
	/**
	 * Parses required item list.
	 * 
	 * @param obj
	 *            object to extract a list from.
	 * @param field
	 *            field to parse.
	 * @param parser
	 *            single item parser.
	 * @return parsed objects.
	 * @throws JSONException
	 *             if format is invalid.
	 */
	public static <T> List<T> getListNotNull(JSONObject obj, String field,
			JsonObjectParser<T> parser) throws JSONException {
		final JSONArray items = getArrayNotNull(obj, field);
		final int length = items.length();
		final List<T> result = new ArrayList<>(length);
		for (int i = 0; i < length; i++)
			result.add(parser.parse(items.getJSONObject(i)));
		return result;
	}

	/**
	 * Parses optional item list.
	 * 
	 * @param obj
	 *            object to extract a list from.
	 * @param field
	 *            field to parse.
	 * @param parser
	 *            single item parser.
	 * @return parsed objects.
	 * @throws JSONException
	 *             if format is invalid.
	 */
	public static <T> List<T> getListOrNull(JSONObject obj, String field,
			JsonObjectParser<T> parser) throws JSONException {
		if (!obj.has(field) || obj.isNull(field))
			return null;
		final JSONArray items = obj.getJSONArray(field);
		final int length = items.length();
		final List<T> result = new ArrayList<>(length);
		for (int i = 0; i < length; i++)
			result.add(parser.parse(items.getJSONObject(i)));
		return result;
	}

	/**
	 * Parses optional item list.
	 * 
	 * @param obj
	 *            object to extract a list from.
	 * @param field
	 *            field to parse.
	 * @param parser
	 *            single item parser.
	 * @return parsed objects.
	 * @throws JSONException
	 *             if format is invalid.
	 */
	public static <T> List<T> getListOrEmpty(JSONObject obj, String field,
			JsonObjectParser<T> parser) throws JSONException {
		if (!obj.has(field) || obj.isNull(field))
			return new ArrayList<>();
		final JSONArray items = obj.getJSONArray(field);
		if (items == null)
			return new ArrayList<>();
		final int length = items.length();
		final List<T> result = new ArrayList<>(length);
		for (int i = 0; i < length; i++)
			result.add(parser.parse(items.getJSONObject(i)));
		return result;
	}

	/**
	 * Fetch a date or null.
	 * 
	 * @param obj
	 *            object to get.
	 * @param field
	 *            field to use.
	 * @param dateFormat
	 *            field date format.
	 * @return data format.
	 * @throws JSONException
	 *             if error occurs.
	 */
	public static Date getDateOrNull(JSONObject obj, String field,
			final SimpleDateFormat dateFormat) throws JSONException {
		if (!obj.has(field) || obj.isNull(field))
			return null;
		final String guess = obj.getString(field);
		try {
			return dateFormat.parse(guess);
		} catch (ParseException e) {
			throw new JSONException("Bad date value " + guess);
		}
	}

	/**
	 * Fetches an optional string from an object.
	 * 
	 * @param obj
	 *            object to get a field from.
	 * @param field
	 *            field to get a value from.
	 * @throws JSONException
	 *             if value is not valid
	 */
	public static String getStringOrNull(JSONObject obj, String field)
			throws JSONException {
		if (!obj.has(field) || obj.isNull(field))
			return null;
		return obj.getString(field);
	}
    
	/**
	 * Returns an optional "boolean" field value. If field is absent or set to
	 * <code>null</code>, this method returns <code>false</code>.
	 * 
	 * @param obj
	 *            object to get a field from.
	 * @param field
	 *            field to get a value for.
	 * @return boolean value.
	 * @throws JSONException
	 *             if input is not valid (field value is not boolean).
	 */
	public static boolean getOptionalBool(JSONObject obj, String field)
			throws JSONException {
		if (!obj.has(field) || obj.isNull(field)) {
			return false;
		}
		return obj.getBoolean(field);
	}

	/**
	 * Fetches an optional string from an object. Absent value is returned as an
	 * empty string instead of null.
	 * 
	 * @param obj
	 *            object to get a field from.
	 * @param field
	 *            field to get a value from.
	 * @throws JSONException
	 *             if value is not valid
	 */
	public static String getStringOrEmpty(JSONObject obj, String field)
			throws JSONException {
		if (!obj.has(field) || obj.isNull(field))
			return "";
		return obj.getString(field);
	}

	/**
	 * Fetches a string from an object.
	 * 
	 * @param obj
	 *            object to get a field from.
	 * @param field
	 *            field to get a value from.
	 * @throws JSONException
	 *             if value is not valid, not exists, etc...
	 */
	public static String getStringNotNull(JSONObject obj, String field)
			throws JSONException {
		return obj.getString(field);
	}

	/**
	 * Fetches an int from an object.
	 * 
	 * @param obj
	 *            object to get a field from.
	 * @param field
	 *            field to get a value from.
	 * @throws JSONException
	 *             if value is not valid, not exists, etc...
	 */
	public static int getInt(JSONObject obj, String field) throws JSONException {
		return obj.getInt(field);
	}

	/**
	 * Fetches an int from an object.
	 * 
	 * @param obj
	 *            object to get a field from.
	 * @param field
	 *            field to get a value from.
	 * @param deflt
	 *            default value.
	 * @throws JSONException
	 *             if value is not valid, not exists, etc...
	 */
	public static int getInt(JSONObject obj, String field, int deflt)
			throws JSONException {
		return obj.optInt(field, deflt);
	}

	/**
	 * Fetches an optional int from an object.
	 * 
	 * @param obj
	 *            object to get a field from.
	 * @param field
	 *            field to get a value from.
	 * @throws JSONException
	 *             if value is not valid, not exists, etc...
	 */
	public static Integer getIntOrNull(JSONObject obj, String field)
			throws JSONException {
		if (!obj.has(field) || obj.isNull(field))
			return null;
		return obj.getInt(field);
	}

	/**
	 * Fetches a long from an object.
	 * 
	 * @param obj
	 *            object to get a field from.
	 * @param field
	 *            field to get a value from.
	 * @throws JSONException
	 *             if value is not valid, not exists, etc...
	 */
	public static long getLong(JSONObject obj, String field)
			throws JSONException {
		return obj.getLong(field);
	}

	/**
	 * Fetches an optional long from an object.
	 * 
	 * @param obj
	 *            object to get a field from.
	 * @param field
	 *            field to get a value from.
	 * @throws JSONException
	 *             if value is not valid, not exists, etc...
	 */
	public static Long getLongOrNull(JSONObject obj, String field)
			throws JSONException {
		if (!obj.has(field) || obj.isNull(field))
			return null;
		return obj.getLong(field);
	}

	/**
	 * Fetches an optional float from an object.
	 * 
	 * @param obj
	 *            object to get a field from.
	 * @param field
	 *            field to get a value from.
	 * @throws JSONException
	 *             if value is not valid, not exists, etc...
	 */
	public static Float getFloatOrNull(JSONObject obj, String field)
			throws JSONException {
		if (!obj.has(field) || obj.isNull(field))
			return null;
		return (float) obj.getDouble(field);
	}

	/**
	 * Retreive optional object.
	 * 
	 * @param obj
	 *            object to parse.
	 * @param field
	 *            field part.
	 * @param parser
	 *            parset ojbect.
	 * @return parsed object.
	 * @throws JSONException
	 *             if value is not valid.
	 */
	public static <T> T getObjectOrNull(JSONObject obj, String field,
			JsonObjectParser<T> parser) throws JSONException {
		if (!obj.has(field) || obj.isNull(field))
			return null;
		return parser.parse(obj.getJSONObject(field));
	}

	/**
	 * Returns a json array as "not-null" value.
	 * 
	 * @param obj
	 *            object to get a value from.
	 * @param field
	 *            field to get a value from.
	 * @return json array.
	 */
	public static JSONArray getArrayOrNull(JSONObject obj, String field)
			throws JSONException {
		if (!obj.has(field) || obj.isNull(field))
			return null;
		return obj.getJSONArray(field);
	}

	/**
	 * Returns a json array as "not-null" value.
	 * 
	 * @param obj
	 *            object to get a value from.
	 * @param field
	 *            field to get a value from.
	 * @return json array.
	 */
	public static JSONArray getArrayNotNull(JSONObject obj, String field)
			throws JSONException {
		return obj.getJSONArray(field);
	}

	/**
	 * Returns a json object field for a specified object.
	 * 
	 * @param obj
	 *            object to get a field from.
	 * @param field
	 *            returned field.
	 * @return object field.
	 * @throws JSONException
	 *             if target field is not an object.
	 */
	public static JSONObject getObjectNotNull(JSONObject obj, String field)
			throws JSONException {
		return obj.getJSONObject(field);
	}

	/**
	 * Returns a json object field for a specified object.
	 * 
	 * @param obj
	 *            object to get a field from.
	 * @param field
	 *            returned field.
	 * @return object field.
	 * @throws JSONException
	 *             if target field is not an object.
	 */
	public static JSONObject getObjectOrNull(JSONObject obj, String field)
			throws JSONException {
		if (!obj.has(field) || obj.isNull(field))
			return null;
		return obj.getJSONObject(field);
	}
} public interface JsonObjectParser<T> {
	T parse(JSONObject input) throws JSONException;
} public interface JsonObjectWriter<T> {
	void write(JSONWriter writer, T object) throws JSONException;
}
import com.taskadapter.redmineapi.NotFoundException;
import com.taskadapter.redmineapi.RedmineAuthenticationException;
import com.taskadapter.redmineapi.RedmineException;
import com.taskadapter.redmineapi.RedmineFormatException;
import com.taskadapter.redmineapi.RedmineInternalError;
import com.taskadapter.redmineapi.RedmineManager;
import com.taskadapter.redmineapi.bean.Attachment;
import com.taskadapter.redmineapi.bean.CustomFieldDefinition;
import com.taskadapter.redmineapi.bean.File;
import com.taskadapter.redmineapi.bean.FluentStyle;
import com.taskadapter.redmineapi.bean.Group;
import com.taskadapter.redmineapi.bean.Identifiable;
import com.taskadapter.redmineapi.bean.Issue;
import com.taskadapter.redmineapi.bean.IssueCategory;
import com.taskadapter.redmineapi.bean.IssuePriority;
import com.taskadapter.redmineapi.bean.IssueRelation;
import com.taskadapter.redmineapi.bean.IssueStatus;
import com.taskadapter.redmineapi.bean.Membership;
import com.taskadapter.redmineapi.bean.News;
import com.taskadapter.redmineapi.bean.Project;
import com.taskadapter.redmineapi.bean.Role;
import com.taskadapter.redmineapi.bean.SavedQuery;
import com.taskadapter.redmineapi.bean.TimeEntry;
import com.taskadapter.redmineapi.bean.TimeEntryActivity;
import com.taskadapter.redmineapi.bean.Tracker;
import com.taskadapter.redmineapi.bean.User;
import com.taskadapter.redmineapi.bean.Version;
import com.taskadapter.redmineapi.bean.Watcher;
import com.taskadapter.redmineapi.bean.WikiPage;
import com.taskadapter.redmineapi.bean.WikiPageDetail;
import com.taskadapter.redmineapi.internal.comm.BaseCommunicator;
import com.taskadapter.redmineapi.internal.comm.BasicHttpResponse;
import com.taskadapter.redmineapi.internal.comm.Communicator;
import com.taskadapter.redmineapi.internal.comm.Communicators;
import com.taskadapter.redmineapi.internal.comm.ContentHandler;
import com.taskadapter.redmineapi.internal.comm.SimpleCommunicator;
import com.taskadapter.redmineapi.internal.comm.redmine.RedmineAuthenticator;
import com.taskadapter.redmineapi.internal.comm.redmine.RedmineErrorHandler;
import com.taskadapter.redmineapi.internal.json.JsonInput;
import com.taskadapter.redmineapi.internal.json.JsonObjectParser;
import com.taskadapter.redmineapi.internal.json.JsonObjectWriter;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.entity.AbstractHttpEntity;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.entity.StringEntity;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.InputStream;
import java.io.StringWriter;
import java.net.URI;
import java.nio.charset.UnsupportedCharsetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
package com.taskadapter.redmineapi.internal;



public class Transport {
	private static final Map<Class<?>, EntityConfig<?>> OBJECT_CONFIGS = new HashMap<>();
	private static final String CONTENT_TYPE = "application/json; charset=utf-8";
	private static final int DEFAULT_OBJECTS_PER_PAGE = 25;
	private static final String KEY_TOTAL_COUNT = "total_count";
	private static final String KEY_LIMIT = "limit";
	private static final String KEY_OFFSET = "offset";

	private final Logger logger = LoggerFactory.getLogger(RedmineManager.class);
	private SimpleCommunicator<String> communicator;
	private Communicator<BasicHttpResponse> errorCheckingCommunicator;
	private Communicator<HttpResponse> authenticator;

    private String onBehalfOfUser = null;

    static {
		OBJECT_CONFIGS.put(
				Project.class,
				config("project", "projects",
						RedmineJSONBuilder::writeProject,
						RedmineJSONParser::parseProject));
		OBJECT_CONFIGS.put(
				Issue.class,