View Javadoc
1   /**
2    * Copyright (c) 2013-2023, jcabi.com
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met: 1) Redistributions of source code must retain the above
8    * copyright notice, this list of conditions and the following
9    * disclaimer. 2) Redistributions in binary form must reproduce the above
10   * copyright notice, this list of conditions and the following
11   * disclaimer in the documentation and/or other materials provided
12   * with the distribution. 3) Neither the name of the jcabi.com nor
13   * the names of its contributors may be used to endorse or promote
14   * products derived from this software without specific prior written
15   * permission.
16   *
17   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
19   * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
20   * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
21   * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
22   * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25   * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
26   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
28   * OF THE POSSIBILITY OF SUCH DAMAGE.
29   */
30  package com.jcabi.github;
31  
32  import com.jcabi.aspects.Immutable;
33  import com.jcabi.aspects.Loggable;
34  import java.io.IOException;
35  import java.net.URL;
36  import java.text.ParseException;
37  import java.util.ArrayList;
38  import java.util.Collection;
39  import java.util.Date;
40  import javax.json.Json;
41  import javax.json.JsonArray;
42  import javax.json.JsonObject;
43  import lombok.EqualsAndHashCode;
44  import lombok.ToString;
45  
46  /**
47   * Github issue.
48   *
49   * <p>Use a supplementary "smart" decorator to get other properties
50   * from an issue, for example:
51   *
52   * <pre> Issue.Smart issue = new Issue.Smart(origin);
53   * if (issue.isOpen()) {
54   *   issue.close();
55   * }</pre>
56   *
57   * @author Yegor Bugayenko (yegor256@gmail.com)
58   * @version $Id: 0e8e1a7643ccb1db447ae5802825e1d481fdc0d4 $
59   * @since 0.1
60   * @see <a href="https://developer.github.com/v3/issues/">Issues API</a>
61   * @todo #1462:30min Implement lock reason validation. According to
62   *  documentation lock reason must belong to a specific value domain. This
63   *  validation must be performed in lock method and tests must be added to
64   *  ensure that the class is accepting the correct values and rejecting the
65   *  wrong ones.
66   * @checkstyle MultipleStringLiterals (500 lines)
67   */
68  @Immutable
69  @SuppressWarnings
70      (
71          {
72              "PMD.TooManyMethods", "PMD.GodClass", "PMD.ExcessivePublicCount"
73          }
74      )
75  public interface Issue extends Comparable<Issue>, JsonReadable, JsonPatchable {
76  
77      /**
78       * Issue state.
79       */
80      String OPEN_STATE = "open";
81  
82      /**
83       * Issue state.
84       */
85      String CLOSED_STATE = "closed";
86  
87      /**
88       * Repository we're in.
89       * @return Repo
90       */
91      Repo repo();
92  
93      /**
94       * Get its number.
95       * @return Issue number
96       */
97      int number();
98  
99      /**
100      * Get all comments of the issue.
101      * @return Comments
102      * @see <a href="https://developer.github.com/v3/issues/comments/">Issue Comments API</a>
103      */
104     Comments comments();
105 
106     /**
107      * Get all labels of the issue.
108      * @return Labels
109      * @see <a href="https://developer.github.com/v3/issues/labels/">Labels API</a>
110      */
111     IssueLabels labels();
112 
113     /**
114      * Get all events of the issue.
115      * @return Events
116      * @throws IOException If there is any I/O problem
117      * @see <a href="https://developer.github.com/v3/issues/events/#list-events-for-an-issue">List Events for an Issue</a>
118      */
119     Iterable<Event> events() throws IOException;
120 
121     /**
122      * Does this issue exist in Github?
123      * @return TRUE if this issue exists
124      * @throws IOException If there is any I/O problem
125      */
126     boolean exists() throws IOException;
127 
128     /**
129      * Adds the reaction to the issue.
130      * @param reaction Reaction to be added.
131      * @throws IOException If there is any I/O problem
132      */
133     void react(Reaction reaction) throws IOException;
134 
135     /**
136      * List the reactions of the issue.
137      * @return Issue reactions.
138      */
139     Iterable<Reaction> reactions();
140 
141     /**
142      * Locks the issue.
143      * @param reason Lock reason
144      */
145     void lock(String reason);
146 
147     /**
148      * Unlocks the issue.
149      */
150     void unlock();
151 
152     /**
153      * The issue conversation is locked?
154      * @return If the issue is locked.
155      */
156     boolean isLocked();
157 
158     /**
159      * Smart Issue with extra features.
160      */
161     @Immutable
162     @ToString
163     @Loggable(Loggable.DEBUG)
164     @EqualsAndHashCode(of = {"issue", "jsn"})
165     final class Smart implements Issue {
166         /**
167          * Encapsulated issue.
168          */
169         private final transient Issue issue;
170         /**
171          * SmartJson object for convenient JSON parsing.
172          */
173         private final transient SmartJson jsn;
174         /**
175          * Public ctor.
176          * @param iss Issue
177          */
178         public Smart(final Issue iss) {
179             this.issue = iss;
180             this.jsn = new SmartJson(iss);
181         }
182         /**
183          * Get its author.
184          * @return Author of issue (who submitted it)
185          * @throws IOException If there is any I/O problem
186          */
187         public User author() throws IOException {
188             return this.issue.repo().github().users().get(
189                 this.jsn.value(
190                     "user", JsonObject.class
191                 ).getString("login")
192             );
193         }
194         /**
195          * Is it open?
196          * @return TRUE if it's open
197          * @throws IOException If there is any I/O problem
198          */
199         public boolean isOpen() throws IOException {
200             return Issue.OPEN_STATE.equals(this.state());
201         }
202         /**
203          * Open it (make sure it's open).
204          * @throws IOException If there is any I/O problem
205          */
206         public void open() throws IOException {
207             this.state(Issue.OPEN_STATE);
208         }
209         /**
210          * Close it (make sure it's closed).
211          * @throws IOException If there is any I/O problem
212          */
213         public void close() throws IOException {
214             this.state(Issue.CLOSED_STATE);
215         }
216         /**
217          * Get its state.
218          * @return State of issue
219          * @throws IOException If there is any I/O problem
220          */
221         public String state() throws IOException {
222             return this.jsn.text("state");
223         }
224         /**
225          * Change its state.
226          * @param state State of issue
227          * @throws IOException If there is any I/O problem
228          */
229         public void state(final String state) throws IOException {
230             this.issue.patch(
231                 Json.createObjectBuilder().add("state", state).build()
232             );
233         }
234         /**
235          * Get its title.
236          * @return Title of issue
237          * @throws IOException If there is any I/O problem
238          */
239         public String title() throws IOException {
240             return this.jsn.text("title");
241         }
242         /**
243          * Change its title.
244          * @param text Title of issue
245          * @throws IOException If there is any I/O problem
246          */
247         public void title(final String text) throws IOException {
248             this.issue.patch(
249                 Json.createObjectBuilder().add("title", text).build()
250             );
251         }
252         /**
253          * Get its body.
254          * @return Body of issue
255          * @throws IOException If there is any I/O problem
256          */
257         public String body() throws IOException {
258             return this.jsn.text("body");
259         }
260         /**
261          * Change its body.
262          * @param text Body of issue
263          * @throws IOException If there is any I/O problem
264          */
265         public void body(final String text) throws IOException {
266             this.issue.patch(
267                 Json.createObjectBuilder().add("body", text).build()
268             );
269         }
270         /**
271          * Has body?
272          * @return TRUE if body exists
273          * @throws IOException If there is any I/O problem
274          * @since 0.22
275          */
276         public boolean hasBody() throws IOException {
277             return this.jsn.hasNotNull("body");
278         }
279         /**
280          * Has assignee?
281          * @return TRUE if assignee exists
282          * @throws IOException If there is any I/O problem
283          */
284         public boolean hasAssignee() throws IOException {
285             return this.jsn.hasNotNull("assignee");
286         }
287         /**
288          * Get its assignee.
289          * @return User Assignee of issue
290          * @throws IOException If there is any I/O problem
291          */
292         public User assignee() throws IOException {
293             if (!this.hasAssignee()) {
294                 throw new IllegalArgumentException(
295                     String.format(
296                         "issue #%d doesn't have an assignee, use hasAssignee()",
297                         this.number()
298                     )
299                 );
300             }
301             return this.issue.repo().github().users().get(
302                 this.jsn.value(
303                     "assignee", JsonObject.class
304                 ).getString("login")
305             );
306         }
307         /**
308          * Assign this issue to another user.
309          * @param login Login of the user to assign to
310          * @throws IOException If there is any I/O problem
311          */
312         public void assign(final String login) throws IOException {
313             this.issue.patch(
314                 Json.createObjectBuilder().add("assignee", login).build()
315             );
316         }
317         /**
318          * Get its URL.
319          * @return URL of issue
320          * @throws IOException If there is any I/O problem
321          */
322         public URL url() throws IOException {
323             return new URL(this.jsn.text("url"));
324         }
325         /**
326          * Get its HTML URL.
327          * @return URL of issue
328          * @throws IOException If there is any I/O problem
329          */
330         public URL htmlUrl() throws IOException {
331             return new URL(this.jsn.text("html_url"));
332         }
333         /**
334          * When this issue was created.
335          * @return Date of creation
336          * @throws IOException If there is any I/O problem
337          */
338         public Date createdAt() throws IOException {
339             try {
340                 return new Github.Time(
341                     this.jsn.text("created_at")
342                 ).date();
343             } catch (final ParseException ex) {
344                 throw new IllegalStateException(ex);
345             }
346         }
347         /**
348          * When this issue was closed.
349          * @return Date of creation
350          * @throws IOException If there is any I/O problem
351          * @since 0.34
352          */
353         public Date closedAt() throws IOException {
354             try {
355                 return new Github.Time(
356                     this.jsn.text("closed_at")
357                 ).date();
358             } catch (final ParseException ex) {
359                 throw new IllegalStateException(ex);
360             }
361         }
362         /**
363          * When this issue was updated.
364          * @return Date of update
365          * @throws IOException If there is any I/O problem
366          */
367         public Date updatedAt() throws IOException {
368             try {
369                 return new Github.Time(
370                     this.jsn.text("updated_at")
371                 ).date();
372             } catch (final ParseException ex) {
373                 throw new IllegalStateException(ex);
374             }
375         }
376         /**
377          * Is it a pull request?
378          * @return TRUE if it is a pull request
379          * @throws IOException If there is any I/O problem
380          */
381         public boolean isPull() throws IOException {
382             return this.json().containsKey("pull_request")
383                 && !this.jsn.value("pull_request", JsonObject.class)
384                 .isNull("html_url");
385         }
386 
387         /**
388          * Get pull request.
389          * @return Pull request
390          * @throws IOException If there is any I/O problem
391          */
392         public Pull pull() throws IOException {
393             final String url = this.jsn.value(
394                 "pull_request", JsonObject.class
395             ).getString("html_url");
396             return this.issue.repo().pulls().get(
397                 Integer.parseInt(url.substring(url.lastIndexOf('/') + 1))
398             );
399         }
400         /**
401          * Get the latest event of a given type.
402          * Throws {@link IllegalStateException} if the issue has no events of
403          * the given type.
404          * @param type Type of event
405          * @return Latest event of the given type
406          * @throws IOException If there is any I/O problem
407          */
408         public Event latestEvent(final String type) throws IOException {
409             final Iterable<Event.Smart> events = new Smarts<>(
410                 this.issue.events()
411             );
412             Event found = null;
413             for (final Event.Smart event : events) {
414                 if (event.type().equals(type) && (found == null
415                     || found.number() < event.number())) {
416                     found = event;
417                 }
418             }
419             if (found == null) {
420                 throw new IllegalStateException(
421                     String.format(
422                         "event of type '%s' not found in issue #%d",
423                         type, this.issue.number()
424                     )
425                 );
426             }
427             return found;
428         }
429         /**
430          * Get read-only labels.
431          * @return Collection of labels
432          * @throws IOException If there is any I/O problem
433          * @since 0.6.2
434          */
435         public IssueLabels roLabels() throws IOException {
436             final Collection<JsonObject> array =
437                 this.jsn.value("labels", JsonArray.class)
438                     .getValuesAs(JsonObject.class);
439             final Collection<Label> labels = new ArrayList<>(array.size());
440             for (final JsonObject obj : array) {
441                 labels.add(
442                     new Label.Unmodified(
443                         this.repo(),
444                         obj.toString()
445                     )
446                 );
447             }
448             // @checkstyle AnonInnerLength (1 line)
449             return new IssueLabels() {
450                 @Override
451                 public Issue issue() {
452                     return Issue.Smart.this;
453                 }
454                 @Override
455                 public void add(
456                     final Iterable<String> labels) {
457                     throw new UnsupportedOperationException(
458                         "The issue is read-only."
459                     );
460                 }
461                 @Override
462                 public void replace(
463                     final Iterable<String> labels) {
464                     throw new UnsupportedOperationException(
465                         "The issue is read-only."
466                     );
467                 }
468                 @Override
469                 public Iterable<Label> iterate() {
470                     return labels;
471                 }
472                 @Override
473                 public void remove(
474                     final String name) {
475                     throw new UnsupportedOperationException(
476                         "This issue is read-only."
477                     );
478                 }
479                 @Override
480                 public void clear() {
481                     throw new UnsupportedOperationException(
482                         "This issue is read-only."
483                     );
484                 }
485             };
486         }
487         /**
488          * Does issue have milestone?
489          * @return True if has
490          * @throws IOException If fails
491          */
492         public boolean hasMilestone() throws IOException {
493             return this.jsn.hasNotNull("milestone");
494         }
495         /**
496          * Get milestone for this issue.
497          * @return Milestone
498          * @throws IOException If fails
499          */
500         public Milestone milestone() throws IOException {
501             return this.repo().milestones().get(
502                 this.jsn.value("milestone", JsonObject.class)
503                     .getInt("number")
504             );
505         }
506         /**
507          * Add issueto milestone.
508          * @param milestone Milestone
509          * @throws IOException If fails
510          */
511         public void milestone(final Milestone milestone) throws IOException {
512             this.patch(
513                 Json.createObjectBuilder().add(
514                     "milestone", milestone.number()
515                 ).build()
516             );
517         }
518         @Override
519         public Repo repo() {
520             return this.issue.repo();
521         }
522         @Override
523         public int number() {
524             return this.issue.number();
525         }
526         @Override
527         public Comments comments() {
528             return this.issue.comments();
529         }
530         @Override
531         public IssueLabels labels() {
532             return this.issue.labels();
533         }
534         @Override
535         public Iterable<Event> events() throws IOException {
536             return this.issue.events();
537         }
538         @Override
539         public JsonObject json() throws IOException {
540             return this.issue.json();
541         }
542         @Override
543         public void patch(final JsonObject json) throws IOException {
544             this.issue.patch(json);
545         }
546         @Override
547         public int compareTo(final Issue obj) {
548             return this.issue.compareTo(obj);
549         }
550         @Override
551         public boolean exists() throws IOException {
552             return new Existence(this.issue).check();
553         }
554 
555         @Override
556         public void react(final Reaction reaction) {
557             throw new UnsupportedOperationException("react() not implemented");
558         }
559 
560         @Override
561         public Collection<Reaction> reactions() {
562             throw new UnsupportedOperationException(
563                 "reactions() not implemented"
564             );
565         }
566 
567         @Override
568         public void lock(final String reason) {
569             throw new UnsupportedOperationException("lock not implemented");
570         }
571 
572         @Override
573         public void unlock() {
574             throw new UnsupportedOperationException("unlock not implemented");
575         }
576 
577         @Override
578         public boolean isLocked() {
579             throw new UnsupportedOperationException("isLocked not implemented");
580         }
581     }
582 
583 }