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 29KB

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