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

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