You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

TicketModel.java 32KB

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