Paul Martin
2014-10-18 f5d5680b85ff5df5a0a9fc8cf7180401fd4455b2
commit | author | age
5e3521 1 /*
JM 2  * Copyright 2014 gitblit.com.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.gitblit.models;
17
18 import java.io.ByteArrayInputStream;
19 import java.io.ByteArrayOutputStream;
20 import java.io.IOException;
21 import java.io.ObjectInputStream;
22 import java.io.ObjectOutputStream;
23 import java.io.Serializable;
24 import java.io.UnsupportedEncodingException;
25 import java.security.MessageDigest;
26 import java.security.NoSuchAlgorithmException;
27 import java.text.MessageFormat;
28 import java.util.ArrayList;
29 import java.util.Arrays;
30 import java.util.Collection;
31 import java.util.Collections;
32 import java.util.Date;
33 import java.util.HashMap;
34 import java.util.LinkedHashMap;
35 import java.util.LinkedHashSet;
36 import java.util.List;
37 import java.util.Map;
b799d5 38 import java.util.NoSuchElementException;
5e3521 39 import java.util.Set;
JM 40 import java.util.TreeSet;
41 import java.util.regex.Matcher;
42 import java.util.regex.Pattern;
43
44 import org.eclipse.jgit.util.RelativeDateFormatter;
45
46 /**
47  * The Gitblit Ticket model, its component classes, and enums.
48  *
49  * @author James Moger
50  *
51  */
52 public class TicketModel implements Serializable, Comparable<TicketModel> {
53
54     private static final long serialVersionUID = 1L;
55
56     public String project;
57
58     public String repository;
59
60     public long number;
61
62     public Date created;
63
64     public String createdBy;
65
66     public Date updated;
67
68     public String updatedBy;
69
70     public String title;
71
72     public String body;
73
74     public String topic;
75
76     public Type type;
77
78     public Status status;
79
80     public String responsible;
81
82     public String milestone;
83
84     public String mergeSha;
85
86     public String mergeTo;
87
88     public List<Change> changes;
89
90     public Integer insertions;
91
92     public Integer deletions;
93
94     /**
95      * Builds an effective ticket from the collection of changes.  A change may
96      * Add or Subtract information from a ticket, but the collection of changes
97      * is only additive.
98      *
99      * @param changes
100      * @return the effective ticket
101      */
102     public static TicketModel buildTicket(Collection<Change> changes) {
103         TicketModel ticket;
104         List<Change> effectiveChanges = new ArrayList<Change>();
105         Map<String, Change> comments = new HashMap<String, Change>();
106         for (Change change : changes) {
107             if (change.comment != null) {
108                 if (comments.containsKey(change.comment.id)) {
109                     Change original = comments.get(change.comment.id);
110                     Change clone = copy(original);
111                     clone.comment.text = change.comment.text;
112                     clone.comment.deleted = change.comment.deleted;
113                     int idx = effectiveChanges.indexOf(original);
114                     effectiveChanges.remove(original);
115                     effectiveChanges.add(idx, clone);
116                     comments.put(clone.comment.id, clone);
117                 } else {
118                     effectiveChanges.add(change);
119                     comments.put(change.comment.id, change);
120                 }
121             } else {
122                 effectiveChanges.add(change);
123             }
124         }
125
126         // effective ticket
127         ticket = new TicketModel();
128         for (Change change : effectiveChanges) {
129             if (!change.hasComment()) {
130                 // ensure we do not include a deleted comment
131                 change.comment = null;
132             }
133             ticket.applyChange(change);
134         }
135         return ticket;
136     }
137
138     public TicketModel() {
139         // the first applied change set the date appropriately
140         created = new Date(0);
141         changes = new ArrayList<Change>();
142         status = Status.New;
143         type = Type.defaultType;
144     }
145
146     public boolean isOpen() {
147         return !status.isClosed();
148     }
149
150     public boolean isClosed() {
151         return status.isClosed();
152     }
153
154     public boolean isMerged() {
155         return isClosed() && !isEmpty(mergeSha);
156     }
157
158     public boolean isProposal() {
159         return Type.Proposal == type;
160     }
161
162     public boolean isBug() {
163         return Type.Bug == type;
164     }
165
166     public Date getLastUpdated() {
167         return updated == null ? created : updated;
168     }
169
170     public boolean hasPatchsets() {
171         return getPatchsets().size() > 0;
172     }
173
174     /**
175      * Returns true if multiple participants are involved in discussing a ticket.
176      * The ticket creator is excluded from this determination because a
177      * discussion requires more than one participant.
178      *
179      * @return true if this ticket has a discussion
180      */
181     public boolean hasDiscussion() {
182         for (Change change : getComments()) {
183             if (!change.author.equals(createdBy)) {
184                 return true;
185             }
186         }
187         return false;
188     }
189
190     /**
191      * Returns the list of changes with comments.
192      *
193      * @return
194      */
195     public List<Change> getComments() {
196         List<Change> list = new ArrayList<Change>();
197         for (Change change : changes) {
198             if (change.hasComment()) {
199                 list.add(change);
200             }
201         }
202         return list;
203     }
204
205     /**
206      * Returns the list of participants for the ticket.
207      *
208      * @return the list of participants
209      */
210     public List<String> getParticipants() {
211         Set<String> set = new LinkedHashSet<String>();
212         for (Change change : changes) {
213             if (change.isParticipantChange()) {
214                 set.add(change.author);
215             }
216         }
217         if (responsible != null && responsible.length() > 0) {
218             set.add(responsible);
219         }
220         return new ArrayList<String>(set);
221     }
222
223     public boolean hasLabel(String label) {
224         return getLabels().contains(label);
225     }
226
227     public List<String> getLabels() {
228         return getList(Field.labels);
229     }
230
231     public boolean isResponsible(String username) {
232         return username.equals(responsible);
233     }
234
235     public boolean isAuthor(String username) {
236         return username.equals(createdBy);
237     }
238
239     public boolean isReviewer(String username) {
240         return getReviewers().contains(username);
241     }
242
243     public List<String> getReviewers() {
244         return getList(Field.reviewers);
245     }
246
247     public boolean isWatching(String username) {
248         return getWatchers().contains(username);
249     }
250
251     public List<String> getWatchers() {
252         return getList(Field.watchers);
253     }
254
255     public boolean isVoter(String username) {
256         return getVoters().contains(username);
257     }
258
259     public List<String> getVoters() {
260         return getList(Field.voters);
261     }
262
263     public List<String> getMentions() {
264         return getList(Field.mentions);
265     }
266
267     protected List<String> getList(Field field) {
268         Set<String> set = new TreeSet<String>();
269         for (Change change : changes) {
270             if (change.hasField(field)) {
271                 String values = change.getString(field);
272                 for (String value : values.split(",")) {
273                     switch (value.charAt(0)) {
274                     case '+':
275                         set.add(value.substring(1));
276                         break;
277                     case '-':
278                         set.remove(value.substring(1));
279                         break;
280                     default:
281                         set.add(value);
282                     }
283                 }
284             }
285         }
286         if (!set.isEmpty()) {
287             return new ArrayList<String>(set);
288         }
289         return Collections.emptyList();
290     }
291
292     public Attachment getAttachment(String name) {
293         Attachment attachment = null;
294         for (Change change : changes) {
295             if (change.hasAttachments()) {
296                 Attachment a = change.getAttachment(name);
297                 if (a != null) {
298                     attachment = a;
299                 }
300             }
301         }
302         return attachment;
303     }
304
305     public boolean hasAttachments() {
306         for (Change change : changes) {
307             if (change.hasAttachments()) {
308                 return true;
309             }
310         }
311         return false;
312     }
313
314     public List<Attachment> getAttachments() {
315         List<Attachment> list = new ArrayList<Attachment>();
316         for (Change change : changes) {
317             if (change.hasAttachments()) {
318                 list.addAll(change.attachments);
319             }
320         }
321         return list;
322     }
323
324     public List<Patchset> getPatchsets() {
325         List<Patchset> list = new ArrayList<Patchset>();
326         for (Change change : changes) {
327             if (change.patchset != null) {
328                 list.add(change.patchset);
329             }
330         }
331         return list;
332     }
333
334     public List<Patchset> getPatchsetRevisions(int number) {
335         List<Patchset> list = new ArrayList<Patchset>();
336         for (Change change : changes) {
337             if (change.patchset != null) {
338                 if (number == change.patchset.number) {
339                     list.add(change.patchset);
340                 }
341             }
342         }
343         return list;
344     }
345
346     public Patchset getPatchset(String sha) {
347         for (Change change : changes) {
348             if (change.patchset != null) {
349                 if (sha.equals(change.patchset.tip)) {
350                     return change.patchset;
351                 }
352             }
353         }
354         return null;
355     }
356
357     public Patchset getPatchset(int number, int rev) {
358         for (Change change : changes) {
359             if (change.patchset != null) {
360                 if (number == change.patchset.number && rev == change.patchset.rev) {
361                     return change.patchset;
362                 }
363             }
364         }
365         return null;
366     }
367
368     public Patchset getCurrentPatchset() {
369         Patchset patchset = null;
370         for (Change change : changes) {
371             if (change.patchset != null) {
372                 if (patchset == null) {
373                     patchset = change.patchset;
374                 } else if (patchset.compareTo(change.patchset) == 1) {
375                     patchset = change.patchset;
376                 }
377             }
378         }
379         return patchset;
380     }
381
382     public boolean isCurrent(Patchset patchset) {
383         if (patchset == null) {
384             return false;
385         }
386         Patchset curr = getCurrentPatchset();
387         if (curr == null) {
388             return false;
389         }
390         return curr.equals(patchset);
391     }
392
393     public List<Change> getReviews(Patchset patchset) {
394         if (patchset == null) {
395             return Collections.emptyList();
396         }
397         // collect the patchset reviews by author
398         // the last review by the author is the
399         // official review
400         Map<String, Change> reviews = new LinkedHashMap<String, TicketModel.Change>();
401         for (Change change : changes) {
402             if (change.hasReview()) {
403                 if (change.review.isReviewOf(patchset)) {
404                     reviews.put(change.author, change);
405                 }
406             }
407         }
408         return new ArrayList<Change>(reviews.values());
409     }
410
411
412     public boolean isApproved(Patchset patchset) {
413         if (patchset == null) {
414             return false;
415         }
416         boolean approved = false;
417         boolean vetoed = false;
418         for (Change change : getReviews(patchset)) {
419             if (change.hasReview()) {
420                 if (change.review.isReviewOf(patchset)) {
421                     if (Score.approved == change.review.score) {
422                         approved = true;
423                     } else if (Score.vetoed == change.review.score) {
424                         vetoed = true;
425                     }
426                 }
427             }
428         }
429         return approved && !vetoed;
430     }
431
432     public boolean isVetoed(Patchset patchset) {
433         if (patchset == null) {
434             return false;
435         }
436         for (Change change : getReviews(patchset)) {
437             if (change.hasReview()) {
438                 if (change.review.isReviewOf(patchset)) {
439                     if (Score.vetoed == change.review.score) {
440                         return true;
441                     }
442                 }
443             }
444         }
445         return false;
446     }
447
448     public Review getReviewBy(String username) {
449         for (Change change : getReviews(getCurrentPatchset())) {
450             if (change.author.equals(username)) {
451                 return change.review;
452             }
453         }
454         return null;
455     }
456
457     public boolean isPatchsetAuthor(String username) {
458         for (Change change : changes) {
459             if (change.hasPatchset()) {
460                 if (change.author.equals(username)) {
461                     return true;
462                 }
463             }
464         }
465         return false;
466     }
467
468     public void applyChange(Change change) {
469         if (changes.size() == 0) {
470             // first change created the ticket
471             created = change.date;
472             createdBy = change.author;
473             status = Status.New;
474         } else if (created == null || change.date.after(created)) {
475             // track last ticket update
476             updated = change.date;
477             updatedBy = change.author;
478         }
479
480         if (change.isMerge()) {
481             // identify merge patchsets
482             if (isEmpty(responsible)) {
483                 responsible = change.author;
484             }
485             status = Status.Merged;
486         }
487
488         if (change.hasFieldChanges()) {
489             for (Map.Entry<Field, String> entry : change.fields.entrySet()) {
490                 Field field = entry.getKey();
491                 Object value = entry.getValue();
492                 switch (field) {
493                 case type:
494                     type = TicketModel.Type.fromObject(value, type);
495                     break;
496                 case status:
497                     status = TicketModel.Status.fromObject(value, status);
498                     break;
499                 case title:
500                     title = toString(value);
501                     break;
502                 case body:
503                     body = toString(value);
504                     break;
505                 case topic:
506                     topic = toString(value);
507                     break;
508                 case responsible:
509                     responsible = toString(value);
510                     break;
511                 case milestone:
512                     milestone = toString(value);
513                     break;
514                 case mergeTo:
515                     mergeTo = toString(value);
516                     break;
517                 case mergeSha:
518                     mergeSha = toString(value);
519                     break;
520                 default:
521                     // unknown
522                     break;
523                 }
524             }
525         }
526
527         // add the change to the ticket
528         changes.add(change);
529     }
530
531     protected String toString(Object value) {
532         if (value == null) {
533             return null;
534         }
535         return value.toString();
536     }
537
538     public String toIndexableString() {
539         StringBuilder sb = new StringBuilder();
540         if (!isEmpty(title)) {
541             sb.append(title).append('\n');
542         }
543         if (!isEmpty(body)) {
544             sb.append(body).append('\n');
545         }
546         for (Change change : changes) {
547             if (change.hasComment()) {
548                 sb.append(change.comment.text);
549                 sb.append('\n');
550             }
551         }
552         return sb.toString();
553     }
554
555     @Override
556     public String toString() {
557         StringBuilder sb = new StringBuilder();
558         sb.append("#");
559         sb.append(number);
560         sb.append(": " + title + "\n");
561         for (Change change : changes) {
562             sb.append(change);
563             sb.append('\n');
564         }
565         return sb.toString();
566     }
567
568     @Override
569     public int compareTo(TicketModel o) {
570         return o.created.compareTo(created);
571     }
572
573     @Override
574     public boolean equals(Object o) {
575         if (o instanceof TicketModel) {
576             return number == ((TicketModel) o).number;
577         }
578         return super.equals(o);
579     }
580
581     @Override
582     public int hashCode() {
583         return (repository + number).hashCode();
584     }
585
586     /**
587      * Encapsulates a ticket change
588      */
589     public static class Change implements Serializable, Comparable<Change> {
590
591         private static final long serialVersionUID = 1L;
592
593         public final Date date;
594
595         public final String author;
596
597         public Comment comment;
598
599         public Map<Field, String> fields;
600
601         public Set<Attachment> attachments;
602
603         public Patchset patchset;
604
605         public Review review;
606
607         private transient String id;
608
609         public Change(String author) {
610             this(author, new Date());
611         }
612
613         public Change(String author, Date date) {
614             this.date = date;
615             this.author = author;
616         }
617
618         public boolean isStatusChange() {
619             return hasField(Field.status);
620         }
621
622         public Status getStatus() {
623             Status state = Status.fromObject(getField(Field.status), null);
624             return state;
625         }
626
627         public boolean isMerge() {
628             return hasField(Field.status) && hasField(Field.mergeSha);
629         }
630
631         public boolean hasPatchset() {
632             return patchset != null;
633         }
634
635         public boolean hasReview() {
636             return review != null;
637         }
638
639         public boolean hasComment() {
fafeb6 640             return comment != null && !comment.isDeleted() && comment.text != null;
5e3521 641         }
JM 642
643         public Comment comment(String text) {
644             comment = new Comment(text);
645             comment.id = TicketModel.getSHA1(date.toString() + author + text);
646
647             try {
648                 Pattern mentions = Pattern.compile("\\s@([A-Za-z0-9-_]+)");
649                 Matcher m = mentions.matcher(text);
650                 while (m.find()) {
651                     String username = m.group(1);
652                     plusList(Field.mentions, username);
653                 }
654             } catch (Exception e) {
655                 // ignore
656             }
657             return comment;
658         }
659
660         public Review review(Patchset patchset, Score score, boolean addReviewer) {
661             if (addReviewer) {
662                 plusList(Field.reviewers, author);
663             }
664             review = new Review(patchset.number, patchset.rev);
665             review.score = score;
666             return review;
667         }
668
669         public boolean hasAttachments() {
670             return !TicketModel.isEmpty(attachments);
671         }
672
673         public void addAttachment(Attachment attachment) {
674             if (attachments == null) {
675                 attachments = new LinkedHashSet<Attachment>();
676             }
677             attachments.add(attachment);
678         }
679
680         public Attachment getAttachment(String name) {
681             if (attachments != null) {
682                 for (Attachment attachment : attachments) {
683                     if (attachment.name.equalsIgnoreCase(name)) {
684                         return attachment;
685                     }
686                 }
687             }
688             return null;
689         }
690
691         public boolean isParticipantChange() {
692             if (hasComment()
693                     || hasReview()
694                     || hasPatchset()
695                     || hasAttachments()) {
696                 return true;
697             }
698
699             if (TicketModel.isEmpty(fields)) {
700                 return false;
701             }
702
703             // identify real ticket field changes
704             Map<Field, String> map = new HashMap<Field, String>(fields);
705             map.remove(Field.watchers);
706             map.remove(Field.voters);
707             return !map.isEmpty();
708         }
709
710         public boolean hasField(Field field) {
711             return !TicketModel.isEmpty(getString(field));
712         }
713
714         public boolean hasFieldChanges() {
715             return !TicketModel.isEmpty(fields);
716         }
717
718         public String getField(Field field) {
719             if (fields != null) {
720                 return fields.get(field);
721             }
722             return null;
723         }
724
725         public void setField(Field field, Object value) {
726             if (fields == null) {
727                 fields = new LinkedHashMap<Field, String>();
728             }
729             if (value == null) {
730                 fields.put(field, null);
731             } else if (Enum.class.isAssignableFrom(value.getClass())) {
732                 fields.put(field, ((Enum<?>) value).name());
733             } else {
734                 fields.put(field, value.toString());
735             }
736         }
737
738         public void remove(Field field) {
739             if (fields != null) {
740                 fields.remove(field);
741             }
742         }
743
744         public String getString(Field field) {
745             String value = getField(field);
746             if (value == null) {
747                 return null;
748             }
749             return value;
750         }
751
752         public void watch(String... username) {
753             plusList(Field.watchers, username);
754         }
755
756         public void unwatch(String... username) {
757             minusList(Field.watchers, username);
758         }
759
760         public void vote(String... username) {
761             plusList(Field.voters, username);
762         }
763
764         public void unvote(String... username) {
765             minusList(Field.voters, username);
766         }
767
768         public void label(String... label) {
769             plusList(Field.labels, label);
770         }
771
772         public void unlabel(String... label) {
773             minusList(Field.labels, label);
774         }
775
776         protected void plusList(Field field, String... items) {
777             modList(field, "+", items);
778         }
779
780         protected void minusList(Field field, String... items) {
781             modList(field, "-", items);
782         }
783
784         private void modList(Field field, String prefix, String... items) {
785             List<String> list = new ArrayList<String>();
786             for (String item : items) {
787                 list.add(prefix + item);
788             }
120794 789             if (hasField(field)) {
JM 790                 String flat = getString(field);
791                 if (isEmpty(flat)) {
792                     // field is empty, use this list
793                     setField(field, join(list, ","));
794                 } else {
795                     // merge this list into the existing field list
796                     Set<String> set = new TreeSet<String>(Arrays.asList(flat.split(",")));
797                     set.addAll(list);
798                     setField(field, join(set, ","));
799                 }
800             } else {
801                 // does not have a list for this field
802                 setField(field, join(list, ","));
803             }
5e3521 804         }
JM 805
806         public String getId() {
807             if (id == null) {
808                 id = getSHA1(Long.toHexString(date.getTime()) + author);
809             }
810             return id;
811         }
812
813         @Override
814         public int compareTo(Change c) {
815             return date.compareTo(c.date);
816         }
817
818         @Override
819         public int hashCode() {
820             return getId().hashCode();
821         }
822
823         @Override
824         public boolean equals(Object o) {
825             if (o instanceof Change) {
826                 return getId().equals(((Change) o).getId());
827             }
828             return false;
829         }
830
831         @Override
832         public String toString() {
833             StringBuilder sb = new StringBuilder();
834             sb.append(RelativeDateFormatter.format(date));
835             if (hasComment()) {
836                 sb.append(" commented on by ");
837             } else if (hasPatchset()) {
838                 sb.append(MessageFormat.format(" {0} uploaded by ", patchset));
839             } else {
840                 sb.append(" changed by ");
841             }
842             sb.append(author).append(" - ");
843             if (hasComment()) {
844                 if (comment.isDeleted()) {
845                     sb.append("(deleted) ");
846                 }
847                 sb.append(comment.text).append(" ");
848             }
849
850             if (hasFieldChanges()) {
851                 for (Map.Entry<Field, String> entry : fields.entrySet()) {
852                     sb.append("\n  ");
853                     sb.append(entry.getKey().name());
854                     sb.append(':');
855                     sb.append(entry.getValue());
856                 }
857             }
858             return sb.toString();
859         }
860     }
861
862     /**
863      * Returns true if the string is null or empty.
864      *
865      * @param value
866      * @return true if string is null or empty
867      */
868     static boolean isEmpty(String value) {
869         return value == null || value.trim().length() == 0;
870     }
871
872     /**
873      * Returns true if the collection is null or empty
874      *
875      * @param collection
876      * @return
877      */
878     static boolean isEmpty(Collection<?> collection) {
879         return collection == null || collection.size() == 0;
880     }
881
882     /**
883      * Returns true if the map is null or empty
884      *
885      * @param map
886      * @return
887      */
888     static boolean isEmpty(Map<?, ?> map) {
889         return map == null || map.size() == 0;
890     }
891
892     /**
893      * Calculates the SHA1 of the string.
894      *
895      * @param text
896      * @return sha1 of the string
897      */
898     static String getSHA1(String text) {
899         try {
900             byte[] bytes = text.getBytes("iso-8859-1");
901             return getSHA1(bytes);
902         } catch (UnsupportedEncodingException u) {
903             throw new RuntimeException(u);
904         }
905     }
906
907     /**
908      * Calculates the SHA1 of the byte array.
909      *
910      * @param bytes
911      * @return sha1 of the byte array
912      */
913     static String getSHA1(byte[] bytes) {
914         try {
915             MessageDigest md = MessageDigest.getInstance("SHA-1");
916             md.update(bytes, 0, bytes.length);
917             byte[] digest = md.digest();
918             return toHex(digest);
919         } catch (NoSuchAlgorithmException t) {
920             throw new RuntimeException(t);
921         }
922     }
923
924     /**
925      * Returns the hex representation of the byte array.
926      *
927      * @param bytes
928      * @return byte array as hex string
929      */
930     static String toHex(byte[] bytes) {
931         StringBuilder sb = new StringBuilder(bytes.length * 2);
932         for (int i = 0; i < bytes.length; i++) {
933             if ((bytes[i] & 0xff) < 0x10) {
934                 sb.append('0');
935             }
936             sb.append(Long.toString(bytes[i] & 0xff, 16));
937         }
938         return sb.toString();
939     }
940
941     /**
942      * Join the list of strings into a single string with a space separator.
943      *
944      * @param values
945      * @return joined list
946      */
947     static String join(Collection<String> values) {
948         return join(values, " ");
949     }
950
951     /**
952      * Join the list of strings into a single string with the specified
953      * separator.
954      *
955      * @param values
956      * @param separator
957      * @return joined list
958      */
959     static String join(String[]  values, String separator) {
960         return join(Arrays.asList(values), separator);
961     }
962
963     /**
964      * Join the list of strings into a single string with the specified
965      * separator.
966      *
967      * @param values
968      * @param separator
969      * @return joined list
970      */
971     static String join(Collection<String> values, String separator) {
972         StringBuilder sb = new StringBuilder();
973         for (String value : values) {
974             sb.append(value).append(separator);
975         }
976         if (sb.length() > 0) {
977             // truncate trailing separator
978             sb.setLength(sb.length() - separator.length());
979         }
980         return sb.toString().trim();
981     }
982
983
984     /**
985      * Produce a deep copy of the given object. Serializes the entire object to
986      * a byte array in memory. Recommended for relatively small objects.
987      */
988     @SuppressWarnings("unchecked")
989     static <T> T copy(T original) {
990         T o = null;
991         try {
992             ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
993             ObjectOutputStream oos = new ObjectOutputStream(byteOut);
994             oos.writeObject(original);
995             ByteArrayInputStream byteIn = new ByteArrayInputStream(byteOut.toByteArray());
996             ObjectInputStream ois = new ObjectInputStream(byteIn);
997             try {
998                 o = (T) ois.readObject();
999             } catch (ClassNotFoundException cex) {
1000                 // actually can not happen in this instance
1001             }
1002         } catch (IOException iox) {
1003             // doesn't seem likely to happen as these streams are in memory
1004             throw new RuntimeException(iox);
1005         }
1006         return o;
1007     }
1008
1009     public static class Patchset implements Serializable, Comparable<Patchset> {
1010
1011         private static final long serialVersionUID = 1L;
1012
1013         public int number;
1014         public int rev;
1015         public String tip;
1016         public String parent;
1017         public String base;
1018         public int insertions;
1019         public int deletions;
1020         public int commits;
1021         public int added;
1022         public PatchsetType type;
1023
1024         public boolean isFF() {
1025             return PatchsetType.FastForward == type;
1026         }
1027
1028         @Override
1029         public int hashCode() {
1030             return toString().hashCode();
1031         }
1032
1033         @Override
1034         public boolean equals(Object o) {
1035             if (o instanceof Patchset) {
1036                 return hashCode() == o.hashCode();
1037             }
1038             return false;
1039         }
1040
1041         @Override
1042         public int compareTo(Patchset p) {
1043             if (number > p.number) {
1044                 return -1;
1045             } else if (p.number > number) {
1046                 return 1;
1047             } else {
1048                 // same patchset, different revision
1049                 if (rev > p.rev) {
1050                     return -1;
1051                 } else if (p.rev > rev) {
1052                     return 1;
1053                 } else {
1054                     // same patchset & revision
1055                     return 0;
1056                 }
1057             }
1058         }
1059
1060         @Override
1061         public String toString() {
1062             return "patchset " + number + " revision " + rev;
1063         }
1064     }
1065
1066     public static class Comment implements Serializable {
1067
1068         private static final long serialVersionUID = 1L;
1069
1070         public String text;
1071
1072         public String id;
1073
1074         public Boolean deleted;
1075
1076         public CommentSource src;
1077
1078         public String replyTo;
1079
1080         Comment(String text) {
1081             this.text = text;
1082         }
1083
1084         public boolean isDeleted() {
1085             return deleted != null && deleted;
1086         }
1087
1088         @Override
1089         public String toString() {
1090             return text;
1091         }
1092     }
1093
1094     public static class Attachment implements Serializable {
1095
1096         private static final long serialVersionUID = 1L;
1097
1098         public final String name;
1099         public long size;
1100         public byte[] content;
1101         public Boolean deleted;
1102
1103         public Attachment(String name) {
1104             this.name = name;
1105         }
1106
1107         public boolean isDeleted() {
1108             return deleted != null && deleted;
1109         }
1110
1111         @Override
1112         public int hashCode() {
1113             return name.hashCode();
1114         }
1115
1116         @Override
1117         public boolean equals(Object o) {
1118             if (o instanceof Attachment) {
1119                 return name.equalsIgnoreCase(((Attachment) o).name);
1120             }
1121             return false;
1122         }
1123
1124         @Override
1125         public String toString() {
1126             return name;
1127         }
1128     }
1129
1130     public static class Review implements Serializable {
1131
1132         private static final long serialVersionUID = 1L;
1133
1134         public final int patchset;
1135
1136         public final int rev;
1137
1138         public Score score;
1139
1140         public Review(int patchset, int revision) {
1141             this.patchset = patchset;
1142             this.rev = revision;
1143         }
1144
1145         public boolean isReviewOf(Patchset p) {
1146             return patchset == p.number && rev == p.rev;
1147         }
1148
1149         @Override
1150         public String toString() {
1151             return "review of patchset " + patchset + " rev " + rev + ":" + score;
1152         }
1153     }
1154
1155     public static enum Score {
b799d5 1156         approved(2), looks_good(1), not_reviewed(0), needs_improvement(-1), vetoed(
DO 1157                 -2);
5e3521 1158
JM 1159         final int value;
1160
1161         Score(int value) {
1162             this.value = value;
1163         }
1164
1165         public int getValue() {
1166             return value;
1167         }
1168
1169         @Override
1170         public String toString() {
1171             return name().toLowerCase().replace('_', ' ');
1172         }
b799d5 1173
DO 1174         public static Score fromScore(int score) {
1175             for (Score s : values()) {
1176                 if (s.getValue() == score) {
1177                     return s;
1178                 }
1179             }
1180             throw new NoSuchElementException(String.valueOf(score));
1181         }
5e3521 1182     }
JM 1183
1184     public static enum Field {
1185         title, body, responsible, type, status, milestone, mergeSha, mergeTo,
1186         topic, labels, watchers, reviewers, voters, mentions;
1187     }
1188
1189     public static enum Type {
f5d568 1190         Enhancement, Task, Bug, Proposal, Question, Maintenance;
5e3521 1191
JM 1192         public static Type defaultType = Task;
1193
1194         public static Type [] choices() {
f5d568 1195             return new Type [] { Enhancement, Task, Bug, Question, Maintenance };
5e3521 1196         }
JM 1197
1198         @Override
1199         public String toString() {
1200             return name().toLowerCase().replace('_', ' ');
1201         }
1202
1203         public static Type fromObject(Object o, Type defaultType) {
1204             if (o instanceof Type) {
1205                 // cast and return
1206                 return (Type) o;
1207             } else if (o instanceof String) {
1208                 // find by name
1209                 for (Type type : values()) {
1210                     String str = o.toString();
1211                     if (type.name().equalsIgnoreCase(str)
1212                             || type.toString().equalsIgnoreCase(str)) {
1213                         return type;
1214                     }
1215                 }
1216             } else if (o instanceof Number) {
1217                 // by ordinal
1218                 int id = ((Number) o).intValue();
1219                 if (id >= 0 && id < values().length) {
1220                     return values()[id];
1221                 }
1222             }
1223
1224             return defaultType;
1225         }
1226     }
1227
1228     public static enum Status {
4881b8 1229         New, Open, Closed, Resolved, Fixed, Merged, Wontfix, Declined, Duplicate, Invalid, Abandoned, On_Hold, No_Change_Required;
5e3521 1230
4881b8 1231         public static Status [] requestWorkflow = { Open, Resolved, Declined, Duplicate, Invalid, Abandoned, On_Hold, No_Change_Required };
5e3521 1232
4881b8 1233         public static Status [] bugWorkflow = { Open, Fixed, Wontfix, Duplicate, Invalid, Abandoned, On_Hold, No_Change_Required };
5e3521 1234
4881b8 1235         public static Status [] proposalWorkflow = { Open, Resolved, Declined, Abandoned, On_Hold, No_Change_Required };
706251 1236
JM 1237         public static Status [] milestoneWorkflow = { Open, Closed, Abandoned, On_Hold };
5e3521 1238
JM 1239         @Override
1240         public String toString() {
1241             return name().toLowerCase().replace('_', ' ');
1242         }
1243
1244         public static Status fromObject(Object o, Status defaultStatus) {
1245             if (o instanceof Status) {
1246                 // cast and return
1247                 return (Status) o;
1248             } else if (o instanceof String) {
1249                 // find by name
1250                 String name = o.toString();
1251                 for (Status state : values()) {
1252                     if (state.name().equalsIgnoreCase(name)
1253                             || state.toString().equalsIgnoreCase(name)) {
1254                         return state;
1255                     }
1256                 }
1257             } else if (o instanceof Number) {
1258                 // by ordinal
1259                 int id = ((Number) o).intValue();
1260                 if (id >= 0 && id < values().length) {
1261                     return values()[id];
1262                 }
1263             }
1264
1265             return defaultStatus;
1266         }
1267
1268         public boolean isClosed() {
1269             return ordinal() > Open.ordinal();
1270         }
1271     }
1272
1273     public static enum CommentSource {
1274         Comment, Email
1275     }
1276
1277     public static enum PatchsetType {
1278         Proposal, FastForward, Rebase, Squash, Rebase_Squash, Amend;
1279
1280         public boolean isRewrite() {
1281             return (this != FastForward) && (this != Proposal);
1282         }
1283
1284         @Override
1285         public String toString() {
1286             return name().toLowerCase().replace('_', '+');
1287         }
1288
1289         public static PatchsetType fromObject(Object o) {
1290             if (o instanceof PatchsetType) {
1291                 // cast and return
1292                 return (PatchsetType) o;
1293             } else if (o instanceof String) {
1294                 // find by name
1295                 String name = o.toString();
1296                 for (PatchsetType type : values()) {
1297                     if (type.name().equalsIgnoreCase(name)
1298                             || type.toString().equalsIgnoreCase(name)) {
1299                         return type;
1300                     }
1301                 }
1302             } else if (o instanceof Number) {
1303                 // by ordinal
1304                 int id = ((Number) o).intValue();
1305                 if (id >= 0 && id < values().length) {
1306                     return values()[id];
1307                 }
1308             }
1309
1310             return null;
1311         }
1312     }
1313 }