| | |
| | | /*
|
| | | * Copyright 2011 gitblit.com.
|
| | | *
|
| | | * Licensed under the Apache License, Version 2.0 (the "License");
|
| | | * you may not use this file except in compliance with the License.
|
| | | * You may obtain a copy of the License at
|
| | | *
|
| | | * http://www.apache.org/licenses/LICENSE-2.0
|
| | | *
|
| | | * Unless required by applicable law or agreed to in writing, software
|
| | | * distributed under the License is distributed on an "AS IS" BASIS,
|
| | | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| | | * See the License for the specific language governing permissions and
|
| | | * limitations under the License.
|
| | | */
|
| | | package com.gitblit.models;
|
| | |
|
| | | import java.io.Serializable;
|
| | | import java.text.ParseException;
|
| | | import java.util.ArrayList;
|
| | | import java.util.Date;
|
| | | import java.util.List;
|
| | |
|
| | | /**
|
| | | * TicketModel is a serializable model class that represents a Ticgit ticket.
|
| | | * |
| | | * @author James Moger
|
| | | * |
| | | */
|
| | | public class TicketModel implements Serializable, Comparable<TicketModel> {
|
| | |
|
| | | private static final long serialVersionUID = 1L;
|
| | |
|
| | | public String id;
|
| | | public String name;
|
| | | public String title;
|
| | | public String state;
|
| | | public Date date;
|
| | | public String handler;
|
| | | public String milestone;
|
| | | public String email;
|
| | | public String author;
|
| | | public List<Comment> comments;
|
| | | public List<String> tags;
|
| | |
|
| | | public TicketModel(String ticketName) throws ParseException {
|
| | | state = "";
|
| | | name = ticketName;
|
| | | comments = new ArrayList<Comment>();
|
| | | tags = new ArrayList<String>();
|
| | |
|
| | | String[] chunks = name.split("_");
|
| | | if (chunks.length == 3) {
|
| | | date = new Date(Long.parseLong(chunks[0]) * 1000L);
|
| | | title = chunks[1].replace('-', ' ');
|
| | | }
|
| | | }
|
| | |
|
| | | @Override
|
| | | public int hashCode() {
|
| | | return id.hashCode();
|
| | | }
|
| | |
|
| | | @Override
|
| | | public boolean equals(Object o) {
|
| | | if (o instanceof TicketModel) {
|
| | | TicketModel other = (TicketModel) o;
|
| | | return id.equals(other.id);
|
| | | }
|
| | | return super.equals(o);
|
| | | }
|
| | |
|
| | | @Override
|
| | | public int compareTo(TicketModel o) {
|
| | | return date.compareTo(o.date);
|
| | | }
|
| | |
|
| | | /**
|
| | | * Comment is a serializable model class that represents a Ticgit ticket
|
| | | * comment.
|
| | | * |
| | | * @author James Moger
|
| | | * |
| | | */
|
| | | public static class Comment implements Serializable, Comparable<Comment> {
|
| | |
|
| | | private static final long serialVersionUID = 1L;
|
| | |
|
| | | public String text;
|
| | | public String author;
|
| | | public Date date;
|
| | |
|
| | | public Comment(String filename, String content) throws ParseException {
|
| | | String[] chunks = filename.split("_", -1);
|
| | | this.date = new Date(Long.parseLong(chunks[1]) * 1000L);
|
| | | this.author = chunks[2];
|
| | | this.text = content;
|
| | | }
|
| | |
|
| | | @Override
|
| | | public int hashCode() {
|
| | | return text.hashCode();
|
| | | }
|
| | |
|
| | | @Override
|
| | | public boolean equals(Object o) {
|
| | | if (o instanceof Comment) {
|
| | | Comment other = (Comment) o;
|
| | | return text.equals(other.text);
|
| | | }
|
| | | return super.equals(o);
|
| | | }
|
| | |
|
| | | @Override
|
| | | public int compareTo(Comment o) {
|
| | | return date.compareTo(o.date);
|
| | | }
|
| | | }
|
| | | }
|
| | | /* |
| | | * Copyright 2014 gitblit.com. |
| | | * |
| | | * Licensed under the Apache License, Version 2.0 (the "License"); |
| | | * you may not use this file except in compliance with the License. |
| | | * You may obtain a copy of the License at |
| | | * |
| | | * http://www.apache.org/licenses/LICENSE-2.0 |
| | | * |
| | | * Unless required by applicable law or agreed to in writing, software |
| | | * distributed under the License is distributed on an "AS IS" BASIS, |
| | | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| | | * See the License for the specific language governing permissions and |
| | | * limitations under the License. |
| | | */ |
| | | package com.gitblit.models; |
| | | |
| | | import java.io.ByteArrayInputStream; |
| | | import java.io.ByteArrayOutputStream; |
| | | import java.io.IOException; |
| | | import java.io.ObjectInputStream; |
| | | import java.io.ObjectOutputStream; |
| | | import java.io.Serializable; |
| | | import java.io.UnsupportedEncodingException; |
| | | import java.security.MessageDigest; |
| | | import java.security.NoSuchAlgorithmException; |
| | | import java.text.MessageFormat; |
| | | import java.util.ArrayList; |
| | | import java.util.Arrays; |
| | | import java.util.Collection; |
| | | import java.util.Collections; |
| | | import java.util.Date; |
| | | import java.util.HashMap; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.LinkedHashSet; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | import java.util.TreeSet; |
| | | import java.util.regex.Matcher; |
| | | import java.util.regex.Pattern; |
| | | |
| | | import org.eclipse.jgit.util.RelativeDateFormatter; |
| | | |
| | | /** |
| | | * The Gitblit Ticket model, its component classes, and enums. |
| | | * |
| | | * @author James Moger |
| | | * |
| | | */ |
| | | public class TicketModel implements Serializable, Comparable<TicketModel> { |
| | | |
| | | private static final long serialVersionUID = 1L; |
| | | |
| | | public String project; |
| | | |
| | | public String repository; |
| | | |
| | | public long number; |
| | | |
| | | public Date created; |
| | | |
| | | public String createdBy; |
| | | |
| | | public Date updated; |
| | | |
| | | public String updatedBy; |
| | | |
| | | public String title; |
| | | |
| | | public String body; |
| | | |
| | | public String topic; |
| | | |
| | | public Type type; |
| | | |
| | | public Status status; |
| | | |
| | | public String responsible; |
| | | |
| | | public String milestone; |
| | | |
| | | public String mergeSha; |
| | | |
| | | public String mergeTo; |
| | | |
| | | public List<Change> changes; |
| | | |
| | | public Integer insertions; |
| | | |
| | | public Integer deletions; |
| | | |
| | | /** |
| | | * Builds an effective ticket from the collection of changes. A change may |
| | | * Add or Subtract information from a ticket, but the collection of changes |
| | | * is only additive. |
| | | * |
| | | * @param changes |
| | | * @return the effective ticket |
| | | */ |
| | | public static TicketModel buildTicket(Collection<Change> changes) { |
| | | TicketModel ticket; |
| | | List<Change> effectiveChanges = new ArrayList<Change>(); |
| | | Map<String, Change> comments = new HashMap<String, Change>(); |
| | | for (Change change : changes) { |
| | | if (change.comment != null) { |
| | | if (comments.containsKey(change.comment.id)) { |
| | | Change original = comments.get(change.comment.id); |
| | | Change clone = copy(original); |
| | | clone.comment.text = change.comment.text; |
| | | clone.comment.deleted = change.comment.deleted; |
| | | int idx = effectiveChanges.indexOf(original); |
| | | effectiveChanges.remove(original); |
| | | effectiveChanges.add(idx, clone); |
| | | comments.put(clone.comment.id, clone); |
| | | } else { |
| | | effectiveChanges.add(change); |
| | | comments.put(change.comment.id, change); |
| | | } |
| | | } else { |
| | | effectiveChanges.add(change); |
| | | } |
| | | } |
| | | |
| | | // effective ticket |
| | | ticket = new TicketModel(); |
| | | for (Change change : effectiveChanges) { |
| | | if (!change.hasComment()) { |
| | | // ensure we do not include a deleted comment |
| | | change.comment = null; |
| | | } |
| | | ticket.applyChange(change); |
| | | } |
| | | return ticket; |
| | | } |
| | | |
| | | public TicketModel() { |
| | | // the first applied change set the date appropriately |
| | | created = new Date(0); |
| | | changes = new ArrayList<Change>(); |
| | | status = Status.New; |
| | | type = Type.defaultType; |
| | | } |
| | | |
| | | public boolean isOpen() { |
| | | return !status.isClosed(); |
| | | } |
| | | |
| | | public boolean isClosed() { |
| | | return status.isClosed(); |
| | | } |
| | | |
| | | public boolean isMerged() { |
| | | return isClosed() && !isEmpty(mergeSha); |
| | | } |
| | | |
| | | public boolean isProposal() { |
| | | return Type.Proposal == type; |
| | | } |
| | | |
| | | public boolean isBug() { |
| | | return Type.Bug == type; |
| | | } |
| | | |
| | | public Date getLastUpdated() { |
| | | return updated == null ? created : updated; |
| | | } |
| | | |
| | | public boolean hasPatchsets() { |
| | | return getPatchsets().size() > 0; |
| | | } |
| | | |
| | | /** |
| | | * Returns true if multiple participants are involved in discussing a ticket. |
| | | * The ticket creator is excluded from this determination because a |
| | | * discussion requires more than one participant. |
| | | * |
| | | * @return true if this ticket has a discussion |
| | | */ |
| | | public boolean hasDiscussion() { |
| | | for (Change change : getComments()) { |
| | | if (!change.author.equals(createdBy)) { |
| | | return true; |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * Returns the list of changes with comments. |
| | | * |
| | | * @return |
| | | */ |
| | | public List<Change> getComments() { |
| | | List<Change> list = new ArrayList<Change>(); |
| | | for (Change change : changes) { |
| | | if (change.hasComment()) { |
| | | list.add(change); |
| | | } |
| | | } |
| | | return list; |
| | | } |
| | | |
| | | /** |
| | | * Returns the list of participants for the ticket. |
| | | * |
| | | * @return the list of participants |
| | | */ |
| | | public List<String> getParticipants() { |
| | | Set<String> set = new LinkedHashSet<String>(); |
| | | for (Change change : changes) { |
| | | if (change.isParticipantChange()) { |
| | | set.add(change.author); |
| | | } |
| | | } |
| | | if (responsible != null && responsible.length() > 0) { |
| | | set.add(responsible); |
| | | } |
| | | return new ArrayList<String>(set); |
| | | } |
| | | |
| | | public boolean hasLabel(String label) { |
| | | return getLabels().contains(label); |
| | | } |
| | | |
| | | public List<String> getLabels() { |
| | | return getList(Field.labels); |
| | | } |
| | | |
| | | public boolean isResponsible(String username) { |
| | | return username.equals(responsible); |
| | | } |
| | | |
| | | public boolean isAuthor(String username) { |
| | | return username.equals(createdBy); |
| | | } |
| | | |
| | | public boolean isReviewer(String username) { |
| | | return getReviewers().contains(username); |
| | | } |
| | | |
| | | public List<String> getReviewers() { |
| | | return getList(Field.reviewers); |
| | | } |
| | | |
| | | public boolean isWatching(String username) { |
| | | return getWatchers().contains(username); |
| | | } |
| | | |
| | | public List<String> getWatchers() { |
| | | return getList(Field.watchers); |
| | | } |
| | | |
| | | public boolean isVoter(String username) { |
| | | return getVoters().contains(username); |
| | | } |
| | | |
| | | public List<String> getVoters() { |
| | | return getList(Field.voters); |
| | | } |
| | | |
| | | public List<String> getMentions() { |
| | | return getList(Field.mentions); |
| | | } |
| | | |
| | | protected List<String> getList(Field field) { |
| | | Set<String> set = new TreeSet<String>(); |
| | | for (Change change : changes) { |
| | | if (change.hasField(field)) { |
| | | String values = change.getString(field); |
| | | for (String value : values.split(",")) { |
| | | switch (value.charAt(0)) { |
| | | case '+': |
| | | set.add(value.substring(1)); |
| | | break; |
| | | case '-': |
| | | set.remove(value.substring(1)); |
| | | break; |
| | | default: |
| | | set.add(value); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | if (!set.isEmpty()) { |
| | | return new ArrayList<String>(set); |
| | | } |
| | | return Collections.emptyList(); |
| | | } |
| | | |
| | | public Attachment getAttachment(String name) { |
| | | Attachment attachment = null; |
| | | for (Change change : changes) { |
| | | if (change.hasAttachments()) { |
| | | Attachment a = change.getAttachment(name); |
| | | if (a != null) { |
| | | attachment = a; |
| | | } |
| | | } |
| | | } |
| | | return attachment; |
| | | } |
| | | |
| | | public boolean hasAttachments() { |
| | | for (Change change : changes) { |
| | | if (change.hasAttachments()) { |
| | | return true; |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | public List<Attachment> getAttachments() { |
| | | List<Attachment> list = new ArrayList<Attachment>(); |
| | | for (Change change : changes) { |
| | | if (change.hasAttachments()) { |
| | | list.addAll(change.attachments); |
| | | } |
| | | } |
| | | return list; |
| | | } |
| | | |
| | | public List<Patchset> getPatchsets() { |
| | | List<Patchset> list = new ArrayList<Patchset>(); |
| | | for (Change change : changes) { |
| | | if (change.patchset != null) { |
| | | list.add(change.patchset); |
| | | } |
| | | } |
| | | return list; |
| | | } |
| | | |
| | | public List<Patchset> getPatchsetRevisions(int number) { |
| | | List<Patchset> list = new ArrayList<Patchset>(); |
| | | for (Change change : changes) { |
| | | if (change.patchset != null) { |
| | | if (number == change.patchset.number) { |
| | | list.add(change.patchset); |
| | | } |
| | | } |
| | | } |
| | | return list; |
| | | } |
| | | |
| | | public Patchset getPatchset(String sha) { |
| | | for (Change change : changes) { |
| | | if (change.patchset != null) { |
| | | if (sha.equals(change.patchset.tip)) { |
| | | return change.patchset; |
| | | } |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | public Patchset getPatchset(int number, int rev) { |
| | | for (Change change : changes) { |
| | | if (change.patchset != null) { |
| | | if (number == change.patchset.number && rev == change.patchset.rev) { |
| | | return change.patchset; |
| | | } |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | public Patchset getCurrentPatchset() { |
| | | Patchset patchset = null; |
| | | for (Change change : changes) { |
| | | if (change.patchset != null) { |
| | | if (patchset == null) { |
| | | patchset = change.patchset; |
| | | } else if (patchset.compareTo(change.patchset) == 1) { |
| | | patchset = change.patchset; |
| | | } |
| | | } |
| | | } |
| | | return patchset; |
| | | } |
| | | |
| | | public boolean isCurrent(Patchset patchset) { |
| | | if (patchset == null) { |
| | | return false; |
| | | } |
| | | Patchset curr = getCurrentPatchset(); |
| | | if (curr == null) { |
| | | return false; |
| | | } |
| | | return curr.equals(patchset); |
| | | } |
| | | |
| | | public List<Change> getReviews(Patchset patchset) { |
| | | if (patchset == null) { |
| | | return Collections.emptyList(); |
| | | } |
| | | // collect the patchset reviews by author |
| | | // the last review by the author is the |
| | | // official review |
| | | Map<String, Change> reviews = new LinkedHashMap<String, TicketModel.Change>(); |
| | | for (Change change : changes) { |
| | | if (change.hasReview()) { |
| | | if (change.review.isReviewOf(patchset)) { |
| | | reviews.put(change.author, change); |
| | | } |
| | | } |
| | | } |
| | | return new ArrayList<Change>(reviews.values()); |
| | | } |
| | | |
| | | |
| | | public boolean isApproved(Patchset patchset) { |
| | | if (patchset == null) { |
| | | return false; |
| | | } |
| | | boolean approved = false; |
| | | boolean vetoed = false; |
| | | for (Change change : getReviews(patchset)) { |
| | | if (change.hasReview()) { |
| | | if (change.review.isReviewOf(patchset)) { |
| | | if (Score.approved == change.review.score) { |
| | | approved = true; |
| | | } else if (Score.vetoed == change.review.score) { |
| | | vetoed = true; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | return approved && !vetoed; |
| | | } |
| | | |
| | | public boolean isVetoed(Patchset patchset) { |
| | | if (patchset == null) { |
| | | return false; |
| | | } |
| | | for (Change change : getReviews(patchset)) { |
| | | if (change.hasReview()) { |
| | | if (change.review.isReviewOf(patchset)) { |
| | | if (Score.vetoed == change.review.score) { |
| | | return true; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | public Review getReviewBy(String username) { |
| | | for (Change change : getReviews(getCurrentPatchset())) { |
| | | if (change.author.equals(username)) { |
| | | return change.review; |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | public boolean isPatchsetAuthor(String username) { |
| | | for (Change change : changes) { |
| | | if (change.hasPatchset()) { |
| | | if (change.author.equals(username)) { |
| | | return true; |
| | | } |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | public void applyChange(Change change) { |
| | | if (changes.size() == 0) { |
| | | // first change created the ticket |
| | | created = change.date; |
| | | createdBy = change.author; |
| | | status = Status.New; |
| | | } else if (created == null || change.date.after(created)) { |
| | | // track last ticket update |
| | | updated = change.date; |
| | | updatedBy = change.author; |
| | | } |
| | | |
| | | if (change.isMerge()) { |
| | | // identify merge patchsets |
| | | if (isEmpty(responsible)) { |
| | | responsible = change.author; |
| | | } |
| | | status = Status.Merged; |
| | | } |
| | | |
| | | if (change.hasFieldChanges()) { |
| | | for (Map.Entry<Field, String> entry : change.fields.entrySet()) { |
| | | Field field = entry.getKey(); |
| | | Object value = entry.getValue(); |
| | | switch (field) { |
| | | case type: |
| | | type = TicketModel.Type.fromObject(value, type); |
| | | break; |
| | | case status: |
| | | status = TicketModel.Status.fromObject(value, status); |
| | | break; |
| | | case title: |
| | | title = toString(value); |
| | | break; |
| | | case body: |
| | | body = toString(value); |
| | | break; |
| | | case topic: |
| | | topic = toString(value); |
| | | break; |
| | | case responsible: |
| | | responsible = toString(value); |
| | | break; |
| | | case milestone: |
| | | milestone = toString(value); |
| | | break; |
| | | case mergeTo: |
| | | mergeTo = toString(value); |
| | | break; |
| | | case mergeSha: |
| | | mergeSha = toString(value); |
| | | break; |
| | | default: |
| | | // unknown |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | |
| | | // add the change to the ticket |
| | | changes.add(change); |
| | | } |
| | | |
| | | protected String toString(Object value) { |
| | | if (value == null) { |
| | | return null; |
| | | } |
| | | return value.toString(); |
| | | } |
| | | |
| | | public String toIndexableString() { |
| | | StringBuilder sb = new StringBuilder(); |
| | | if (!isEmpty(title)) { |
| | | sb.append(title).append('\n'); |
| | | } |
| | | if (!isEmpty(body)) { |
| | | sb.append(body).append('\n'); |
| | | } |
| | | for (Change change : changes) { |
| | | if (change.hasComment()) { |
| | | sb.append(change.comment.text); |
| | | sb.append('\n'); |
| | | } |
| | | } |
| | | return sb.toString(); |
| | | } |
| | | |
| | | @Override |
| | | public String toString() { |
| | | StringBuilder sb = new StringBuilder(); |
| | | sb.append("#"); |
| | | sb.append(number); |
| | | sb.append(": " + title + "\n"); |
| | | for (Change change : changes) { |
| | | sb.append(change); |
| | | sb.append('\n'); |
| | | } |
| | | return sb.toString(); |
| | | } |
| | | |
| | | @Override |
| | | public int compareTo(TicketModel o) { |
| | | return o.created.compareTo(created); |
| | | } |
| | | |
| | | @Override |
| | | public boolean equals(Object o) { |
| | | if (o instanceof TicketModel) { |
| | | return number == ((TicketModel) o).number; |
| | | } |
| | | return super.equals(o); |
| | | } |
| | | |
| | | @Override |
| | | public int hashCode() { |
| | | return (repository + number).hashCode(); |
| | | } |
| | | |
| | | /** |
| | | * Encapsulates a ticket change |
| | | */ |
| | | public static class Change implements Serializable, Comparable<Change> { |
| | | |
| | | private static final long serialVersionUID = 1L; |
| | | |
| | | public final Date date; |
| | | |
| | | public final String author; |
| | | |
| | | public Comment comment; |
| | | |
| | | public Map<Field, String> fields; |
| | | |
| | | public Set<Attachment> attachments; |
| | | |
| | | public Patchset patchset; |
| | | |
| | | public Review review; |
| | | |
| | | private transient String id; |
| | | |
| | | public Change(String author) { |
| | | this(author, new Date()); |
| | | } |
| | | |
| | | public Change(String author, Date date) { |
| | | this.date = date; |
| | | this.author = author; |
| | | } |
| | | |
| | | public boolean isStatusChange() { |
| | | return hasField(Field.status); |
| | | } |
| | | |
| | | public Status getStatus() { |
| | | Status state = Status.fromObject(getField(Field.status), null); |
| | | return state; |
| | | } |
| | | |
| | | public boolean isMerge() { |
| | | return hasField(Field.status) && hasField(Field.mergeSha); |
| | | } |
| | | |
| | | public boolean hasPatchset() { |
| | | return patchset != null; |
| | | } |
| | | |
| | | public boolean hasReview() { |
| | | return review != null; |
| | | } |
| | | |
| | | public boolean hasComment() { |
| | | return comment != null && !comment.isDeleted(); |
| | | } |
| | | |
| | | public Comment comment(String text) { |
| | | comment = new Comment(text); |
| | | comment.id = TicketModel.getSHA1(date.toString() + author + text); |
| | | |
| | | try { |
| | | Pattern mentions = Pattern.compile("\\s@([A-Za-z0-9-_]+)"); |
| | | Matcher m = mentions.matcher(text); |
| | | while (m.find()) { |
| | | String username = m.group(1); |
| | | plusList(Field.mentions, username); |
| | | } |
| | | } catch (Exception e) { |
| | | // ignore |
| | | } |
| | | return comment; |
| | | } |
| | | |
| | | public Review review(Patchset patchset, Score score, boolean addReviewer) { |
| | | if (addReviewer) { |
| | | plusList(Field.reviewers, author); |
| | | } |
| | | review = new Review(patchset.number, patchset.rev); |
| | | review.score = score; |
| | | return review; |
| | | } |
| | | |
| | | public boolean hasAttachments() { |
| | | return !TicketModel.isEmpty(attachments); |
| | | } |
| | | |
| | | public void addAttachment(Attachment attachment) { |
| | | if (attachments == null) { |
| | | attachments = new LinkedHashSet<Attachment>(); |
| | | } |
| | | attachments.add(attachment); |
| | | } |
| | | |
| | | public Attachment getAttachment(String name) { |
| | | if (attachments != null) { |
| | | for (Attachment attachment : attachments) { |
| | | if (attachment.name.equalsIgnoreCase(name)) { |
| | | return attachment; |
| | | } |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | public boolean isParticipantChange() { |
| | | if (hasComment() |
| | | || hasReview() |
| | | || hasPatchset() |
| | | || hasAttachments()) { |
| | | return true; |
| | | } |
| | | |
| | | if (TicketModel.isEmpty(fields)) { |
| | | return false; |
| | | } |
| | | |
| | | // identify real ticket field changes |
| | | Map<Field, String> map = new HashMap<Field, String>(fields); |
| | | map.remove(Field.watchers); |
| | | map.remove(Field.voters); |
| | | return !map.isEmpty(); |
| | | } |
| | | |
| | | public boolean hasField(Field field) { |
| | | return !TicketModel.isEmpty(getString(field)); |
| | | } |
| | | |
| | | public boolean hasFieldChanges() { |
| | | return !TicketModel.isEmpty(fields); |
| | | } |
| | | |
| | | public String getField(Field field) { |
| | | if (fields != null) { |
| | | return fields.get(field); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | public void setField(Field field, Object value) { |
| | | if (fields == null) { |
| | | fields = new LinkedHashMap<Field, String>(); |
| | | } |
| | | if (value == null) { |
| | | fields.put(field, null); |
| | | } else if (Enum.class.isAssignableFrom(value.getClass())) { |
| | | fields.put(field, ((Enum<?>) value).name()); |
| | | } else { |
| | | fields.put(field, value.toString()); |
| | | } |
| | | } |
| | | |
| | | public void remove(Field field) { |
| | | if (fields != null) { |
| | | fields.remove(field); |
| | | } |
| | | } |
| | | |
| | | public String getString(Field field) { |
| | | String value = getField(field); |
| | | if (value == null) { |
| | | return null; |
| | | } |
| | | return value; |
| | | } |
| | | |
| | | public void watch(String... username) { |
| | | plusList(Field.watchers, username); |
| | | } |
| | | |
| | | public void unwatch(String... username) { |
| | | minusList(Field.watchers, username); |
| | | } |
| | | |
| | | public void vote(String... username) { |
| | | plusList(Field.voters, username); |
| | | } |
| | | |
| | | public void unvote(String... username) { |
| | | minusList(Field.voters, username); |
| | | } |
| | | |
| | | public void label(String... label) { |
| | | plusList(Field.labels, label); |
| | | } |
| | | |
| | | public void unlabel(String... label) { |
| | | minusList(Field.labels, label); |
| | | } |
| | | |
| | | protected void plusList(Field field, String... items) { |
| | | modList(field, "+", items); |
| | | } |
| | | |
| | | protected void minusList(Field field, String... items) { |
| | | modList(field, "-", items); |
| | | } |
| | | |
| | | private void modList(Field field, String prefix, String... items) { |
| | | List<String> list = new ArrayList<String>(); |
| | | for (String item : items) { |
| | | list.add(prefix + item); |
| | | } |
| | | setField(field, join(list, ",")); |
| | | } |
| | | |
| | | public String getId() { |
| | | if (id == null) { |
| | | id = getSHA1(Long.toHexString(date.getTime()) + author); |
| | | } |
| | | return id; |
| | | } |
| | | |
| | | @Override |
| | | public int compareTo(Change c) { |
| | | return date.compareTo(c.date); |
| | | } |
| | | |
| | | @Override |
| | | public int hashCode() { |
| | | return getId().hashCode(); |
| | | } |
| | | |
| | | @Override |
| | | public boolean equals(Object o) { |
| | | if (o instanceof Change) { |
| | | return getId().equals(((Change) o).getId()); |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | @Override |
| | | public String toString() { |
| | | StringBuilder sb = new StringBuilder(); |
| | | sb.append(RelativeDateFormatter.format(date)); |
| | | if (hasComment()) { |
| | | sb.append(" commented on by "); |
| | | } else if (hasPatchset()) { |
| | | sb.append(MessageFormat.format(" {0} uploaded by ", patchset)); |
| | | } else { |
| | | sb.append(" changed by "); |
| | | } |
| | | sb.append(author).append(" - "); |
| | | if (hasComment()) { |
| | | if (comment.isDeleted()) { |
| | | sb.append("(deleted) "); |
| | | } |
| | | sb.append(comment.text).append(" "); |
| | | } |
| | | |
| | | if (hasFieldChanges()) { |
| | | for (Map.Entry<Field, String> entry : fields.entrySet()) { |
| | | sb.append("\n "); |
| | | sb.append(entry.getKey().name()); |
| | | sb.append(':'); |
| | | sb.append(entry.getValue()); |
| | | } |
| | | } |
| | | return sb.toString(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Returns true if the string is null or empty. |
| | | * |
| | | * @param value |
| | | * @return true if string is null or empty |
| | | */ |
| | | static boolean isEmpty(String value) { |
| | | return value == null || value.trim().length() == 0; |
| | | } |
| | | |
| | | /** |
| | | * Returns true if the collection is null or empty |
| | | * |
| | | * @param collection |
| | | * @return |
| | | */ |
| | | static boolean isEmpty(Collection<?> collection) { |
| | | return collection == null || collection.size() == 0; |
| | | } |
| | | |
| | | /** |
| | | * Returns true if the map is null or empty |
| | | * |
| | | * @param map |
| | | * @return |
| | | */ |
| | | static boolean isEmpty(Map<?, ?> map) { |
| | | return map == null || map.size() == 0; |
| | | } |
| | | |
| | | /** |
| | | * Calculates the SHA1 of the string. |
| | | * |
| | | * @param text |
| | | * @return sha1 of the string |
| | | */ |
| | | static String getSHA1(String text) { |
| | | try { |
| | | byte[] bytes = text.getBytes("iso-8859-1"); |
| | | return getSHA1(bytes); |
| | | } catch (UnsupportedEncodingException u) { |
| | | throw new RuntimeException(u); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Calculates the SHA1 of the byte array. |
| | | * |
| | | * @param bytes |
| | | * @return sha1 of the byte array |
| | | */ |
| | | static String getSHA1(byte[] bytes) { |
| | | try { |
| | | MessageDigest md = MessageDigest.getInstance("SHA-1"); |
| | | md.update(bytes, 0, bytes.length); |
| | | byte[] digest = md.digest(); |
| | | return toHex(digest); |
| | | } catch (NoSuchAlgorithmException t) { |
| | | throw new RuntimeException(t); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Returns the hex representation of the byte array. |
| | | * |
| | | * @param bytes |
| | | * @return byte array as hex string |
| | | */ |
| | | static String toHex(byte[] bytes) { |
| | | StringBuilder sb = new StringBuilder(bytes.length * 2); |
| | | for (int i = 0; i < bytes.length; i++) { |
| | | if ((bytes[i] & 0xff) < 0x10) { |
| | | sb.append('0'); |
| | | } |
| | | sb.append(Long.toString(bytes[i] & 0xff, 16)); |
| | | } |
| | | return sb.toString(); |
| | | } |
| | | |
| | | /** |
| | | * Join the list of strings into a single string with a space separator. |
| | | * |
| | | * @param values |
| | | * @return joined list |
| | | */ |
| | | static String join(Collection<String> values) { |
| | | return join(values, " "); |
| | | } |
| | | |
| | | /** |
| | | * Join the list of strings into a single string with the specified |
| | | * separator. |
| | | * |
| | | * @param values |
| | | * @param separator |
| | | * @return joined list |
| | | */ |
| | | static String join(String[] values, String separator) { |
| | | return join(Arrays.asList(values), separator); |
| | | } |
| | | |
| | | /** |
| | | * Join the list of strings into a single string with the specified |
| | | * separator. |
| | | * |
| | | * @param values |
| | | * @param separator |
| | | * @return joined list |
| | | */ |
| | | static String join(Collection<String> values, String separator) { |
| | | StringBuilder sb = new StringBuilder(); |
| | | for (String value : values) { |
| | | sb.append(value).append(separator); |
| | | } |
| | | if (sb.length() > 0) { |
| | | // truncate trailing separator |
| | | sb.setLength(sb.length() - separator.length()); |
| | | } |
| | | return sb.toString().trim(); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * Produce a deep copy of the given object. Serializes the entire object to |
| | | * a byte array in memory. Recommended for relatively small objects. |
| | | */ |
| | | @SuppressWarnings("unchecked") |
| | | static <T> T copy(T original) { |
| | | T o = null; |
| | | try { |
| | | ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); |
| | | ObjectOutputStream oos = new ObjectOutputStream(byteOut); |
| | | oos.writeObject(original); |
| | | ByteArrayInputStream byteIn = new ByteArrayInputStream(byteOut.toByteArray()); |
| | | ObjectInputStream ois = new ObjectInputStream(byteIn); |
| | | try { |
| | | o = (T) ois.readObject(); |
| | | } catch (ClassNotFoundException cex) { |
| | | // actually can not happen in this instance |
| | | } |
| | | } catch (IOException iox) { |
| | | // doesn't seem likely to happen as these streams are in memory |
| | | throw new RuntimeException(iox); |
| | | } |
| | | return o; |
| | | } |
| | | |
| | | public static class Patchset implements Serializable, Comparable<Patchset> { |
| | | |
| | | private static final long serialVersionUID = 1L; |
| | | |
| | | public int number; |
| | | public int rev; |
| | | public String tip; |
| | | public String parent; |
| | | public String base; |
| | | public int insertions; |
| | | public int deletions; |
| | | public int commits; |
| | | public int added; |
| | | public PatchsetType type; |
| | | |
| | | public boolean isFF() { |
| | | return PatchsetType.FastForward == type; |
| | | } |
| | | |
| | | @Override |
| | | public int hashCode() { |
| | | return toString().hashCode(); |
| | | } |
| | | |
| | | @Override |
| | | public boolean equals(Object o) { |
| | | if (o instanceof Patchset) { |
| | | return hashCode() == o.hashCode(); |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | @Override |
| | | public int compareTo(Patchset p) { |
| | | if (number > p.number) { |
| | | return -1; |
| | | } else if (p.number > number) { |
| | | return 1; |
| | | } else { |
| | | // same patchset, different revision |
| | | if (rev > p.rev) { |
| | | return -1; |
| | | } else if (p.rev > rev) { |
| | | return 1; |
| | | } else { |
| | | // same patchset & revision |
| | | return 0; |
| | | } |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public String toString() { |
| | | return "patchset " + number + " revision " + rev; |
| | | } |
| | | } |
| | | |
| | | public static class Comment implements Serializable { |
| | | |
| | | private static final long serialVersionUID = 1L; |
| | | |
| | | public String text; |
| | | |
| | | public String id; |
| | | |
| | | public Boolean deleted; |
| | | |
| | | public CommentSource src; |
| | | |
| | | public String replyTo; |
| | | |
| | | Comment(String text) { |
| | | this.text = text; |
| | | } |
| | | |
| | | public boolean isDeleted() { |
| | | return deleted != null && deleted; |
| | | } |
| | | |
| | | @Override |
| | | public String toString() { |
| | | return text; |
| | | } |
| | | } |
| | | |
| | | public static class Attachment implements Serializable { |
| | | |
| | | private static final long serialVersionUID = 1L; |
| | | |
| | | public final String name; |
| | | public long size; |
| | | public byte[] content; |
| | | public Boolean deleted; |
| | | |
| | | public Attachment(String name) { |
| | | this.name = name; |
| | | } |
| | | |
| | | public boolean isDeleted() { |
| | | return deleted != null && deleted; |
| | | } |
| | | |
| | | @Override |
| | | public int hashCode() { |
| | | return name.hashCode(); |
| | | } |
| | | |
| | | @Override |
| | | public boolean equals(Object o) { |
| | | if (o instanceof Attachment) { |
| | | return name.equalsIgnoreCase(((Attachment) o).name); |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | @Override |
| | | public String toString() { |
| | | return name; |
| | | } |
| | | } |
| | | |
| | | public static class Review implements Serializable { |
| | | |
| | | private static final long serialVersionUID = 1L; |
| | | |
| | | public final int patchset; |
| | | |
| | | public final int rev; |
| | | |
| | | public Score score; |
| | | |
| | | public Review(int patchset, int revision) { |
| | | this.patchset = patchset; |
| | | this.rev = revision; |
| | | } |
| | | |
| | | public boolean isReviewOf(Patchset p) { |
| | | return patchset == p.number && rev == p.rev; |
| | | } |
| | | |
| | | @Override |
| | | public String toString() { |
| | | return "review of patchset " + patchset + " rev " + rev + ":" + score; |
| | | } |
| | | } |
| | | |
| | | public static enum Score { |
| | | approved(2), looks_good(1), not_reviewed(0), needs_improvement(-1), vetoed(-2); |
| | | |
| | | final int value; |
| | | |
| | | Score(int value) { |
| | | this.value = value; |
| | | } |
| | | |
| | | public int getValue() { |
| | | return value; |
| | | } |
| | | |
| | | @Override |
| | | public String toString() { |
| | | return name().toLowerCase().replace('_', ' '); |
| | | } |
| | | } |
| | | |
| | | public static enum Field { |
| | | title, body, responsible, type, status, milestone, mergeSha, mergeTo, |
| | | topic, labels, watchers, reviewers, voters, mentions; |
| | | } |
| | | |
| | | public static enum Type { |
| | | Enhancement, Task, Bug, Proposal, Question; |
| | | |
| | | public static Type defaultType = Task; |
| | | |
| | | public static Type [] choices() { |
| | | return new Type [] { Enhancement, Task, Bug, Question }; |
| | | } |
| | | |
| | | @Override |
| | | public String toString() { |
| | | return name().toLowerCase().replace('_', ' '); |
| | | } |
| | | |
| | | public static Type fromObject(Object o, Type defaultType) { |
| | | if (o instanceof Type) { |
| | | // cast and return |
| | | return (Type) o; |
| | | } else if (o instanceof String) { |
| | | // find by name |
| | | for (Type type : values()) { |
| | | String str = o.toString(); |
| | | if (type.name().equalsIgnoreCase(str) |
| | | || type.toString().equalsIgnoreCase(str)) { |
| | | return type; |
| | | } |
| | | } |
| | | } else if (o instanceof Number) { |
| | | // by ordinal |
| | | int id = ((Number) o).intValue(); |
| | | if (id >= 0 && id < values().length) { |
| | | return values()[id]; |
| | | } |
| | | } |
| | | |
| | | return defaultType; |
| | | } |
| | | } |
| | | |
| | | public static enum Status { |
| | | New, Open, Resolved, Fixed, Merged, Wontfix, Declined, Duplicate, Invalid, On_Hold; |
| | | |
| | | public static Status [] requestWorkflow = { Open, Resolved, Declined, Duplicate, Invalid, On_Hold }; |
| | | |
| | | public static Status [] bugWorkflow = { Open, Fixed, Wontfix, Duplicate, Invalid, On_Hold }; |
| | | |
| | | public static Status [] proposalWorkflow = { Open, Declined, On_Hold}; |
| | | |
| | | @Override |
| | | public String toString() { |
| | | return name().toLowerCase().replace('_', ' '); |
| | | } |
| | | |
| | | public static Status fromObject(Object o, Status defaultStatus) { |
| | | if (o instanceof Status) { |
| | | // cast and return |
| | | return (Status) o; |
| | | } else if (o instanceof String) { |
| | | // find by name |
| | | String name = o.toString(); |
| | | for (Status state : values()) { |
| | | if (state.name().equalsIgnoreCase(name) |
| | | || state.toString().equalsIgnoreCase(name)) { |
| | | return state; |
| | | } |
| | | } |
| | | } else if (o instanceof Number) { |
| | | // by ordinal |
| | | int id = ((Number) o).intValue(); |
| | | if (id >= 0 && id < values().length) { |
| | | return values()[id]; |
| | | } |
| | | } |
| | | |
| | | return defaultStatus; |
| | | } |
| | | |
| | | public boolean isClosed() { |
| | | return ordinal() > Open.ordinal(); |
| | | } |
| | | } |
| | | |
| | | public static enum CommentSource { |
| | | Comment, Email |
| | | } |
| | | |
| | | public static enum PatchsetType { |
| | | Proposal, FastForward, Rebase, Squash, Rebase_Squash, Amend; |
| | | |
| | | public boolean isRewrite() { |
| | | return (this != FastForward) && (this != Proposal); |
| | | } |
| | | |
| | | @Override |
| | | public String toString() { |
| | | return name().toLowerCase().replace('_', '+'); |
| | | } |
| | | |
| | | public static PatchsetType fromObject(Object o) { |
| | | if (o instanceof PatchsetType) { |
| | | // cast and return |
| | | return (PatchsetType) o; |
| | | } else if (o instanceof String) { |
| | | // find by name |
| | | String name = o.toString(); |
| | | for (PatchsetType type : values()) { |
| | | if (type.name().equalsIgnoreCase(name) |
| | | || type.toString().equalsIgnoreCase(name)) { |
| | | return type; |
| | | } |
| | | } |
| | | } else if (o instanceof Number) { |
| | | // by ordinal |
| | | int id = ((Number) o).intValue(); |
| | | if (id >= 0 && id < values().length) { |
| | | return values()[id]; |
| | | } |
| | | } |
| | | |
| | | return null; |
| | | } |
| | | } |
| | | } |