Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
Issue |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart$1 |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart$AjcClosure1 |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart$AjcClosure11 |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart$AjcClosure13 |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart$AjcClosure15 |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart$AjcClosure17 |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart$AjcClosure19 |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart$AjcClosure21 |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart$AjcClosure23 |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart$AjcClosure25 |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart$AjcClosure27 |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart$AjcClosure29 |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart$AjcClosure3 |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart$AjcClosure31 |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart$AjcClosure33 |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart$AjcClosure35 |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart$AjcClosure37 |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart$AjcClosure39 |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart$AjcClosure41 |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart$AjcClosure43 |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart$AjcClosure45 |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart$AjcClosure47 |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart$AjcClosure49 |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart$AjcClosure5 |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart$AjcClosure51 |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart$AjcClosure53 |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart$AjcClosure55 |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart$AjcClosure57 |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart$AjcClosure59 |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart$AjcClosure61 |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart$AjcClosure63 |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart$AjcClosure65 |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart$AjcClosure67 |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart$AjcClosure69 |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart$AjcClosure7 |
|
| 1.4791666666666667;1.479 | ||||
Issue$Smart$AjcClosure9 |
|
| 1.4791666666666667;1.479 |
1 | 2 | /** |
2 | * Copyright (c) 2013-2017, 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 (yegor@tpc2.com) | |
58 | * @version $Id: c7181ce632dbfefb9c41eda83ac5caaf673d6215 $ | |
59 | * @since 0.1 | |
60 | * @see <a href="http://developer.github.com/v3/issues/">Issues API</a> | |
61 | * @checkstyle MultipleStringLiterals (500 lines) | |
62 | */ | |
63 | @Immutable | |
64 | @SuppressWarnings | |
65 | ( | |
66 | { | |
67 | "PMD.TooManyMethods", "PMD.GodClass", "PMD.ExcessivePublicCount" | |
68 | } | |
69 | ) | |
70 | public interface Issue extends Comparable<Issue>, JsonReadable, JsonPatchable { | |
71 | ||
72 | /** | |
73 | * Issue state. | |
74 | */ | |
75 | String OPEN_STATE = "open"; | |
76 | ||
77 | /** | |
78 | * Issue state. | |
79 | */ | |
80 | String CLOSED_STATE = "closed"; | |
81 | ||
82 | /** | |
83 | * Repository we're in. | |
84 | * @return Repo | |
85 | */ | |
86 | Repo repo(); | |
87 | ||
88 | /** | |
89 | * Get its number. | |
90 | * @return Issue number | |
91 | */ | |
92 | int number(); | |
93 | ||
94 | /** | |
95 | * Get all comments of the issue. | |
96 | * @return Comments | |
97 | * @see <a href="http://developer.github.com/v3/issues/comments/">Issue Comments API</a> | |
98 | */ | |
99 | Comments comments(); | |
100 | ||
101 | /** | |
102 | * Get all labels of the issue. | |
103 | * @return Labels | |
104 | * @see <a href="http://developer.github.com/v3/issues/labels/">Labels API</a> | |
105 | */ | |
106 | IssueLabels labels(); | |
107 | ||
108 | /** | |
109 | * Get all events of the issue. | |
110 | * @return Events | |
111 | * @throws IOException If there is any I/O problem | |
112 | * @see <a href="http://developer.github.com/v3/issues/events/#list-events-for-an-issue">List Events for an Issue</a> | |
113 | */ | |
114 | Iterable<Event> events() throws IOException; | |
115 | ||
116 | /** | |
117 | * Does this issue exist in Github? | |
118 | * @return TRUE if this issue exists | |
119 | * @throws IOException If there is any I/O problem | |
120 | */ | |
121 | boolean exists() throws IOException; | |
122 | ||
123 | /** | |
124 | * Smart Issue with extra features. | |
125 | */ | |
126 | 0 | @Immutable |
127 | 0 | @ToString |
128 | @Loggable(Loggable.DEBUG) | |
129 | 0 | @EqualsAndHashCode(of = {"issue", "jsn"}) |
130 | final class Smart implements Issue { | |
131 | /** | |
132 | * Encapsulated issue. | |
133 | */ | |
134 | private final transient Issue issue; | |
135 | /** | |
136 | * SmartJson object for convenient JSON parsing. | |
137 | */ | |
138 | private final transient SmartJson jsn; | |
139 | /** | |
140 | * Public ctor. | |
141 | * @param iss Issue | |
142 | */ | |
143 | 33 | public Smart(final Issue iss) { |
144 | 33 | this.issue = iss; |
145 | 33 | this.jsn = new SmartJson(iss); |
146 | 33 | } |
147 | /** | |
148 | * Get its author. | |
149 | * @return Author of issue (who submitted it) | |
150 | * @throws IOException If there is any I/O problem | |
151 | */ | |
152 | public User author() throws IOException { | |
153 | 9 | return this.issue.repo().github().users().get( |
154 | 3 | this.jsn.value( |
155 | "user", JsonObject.class | |
156 | 3 | ).getString("login") |
157 | ); | |
158 | } | |
159 | /** | |
160 | * Is it open? | |
161 | * @return TRUE if it's open | |
162 | * @throws IOException If there is any I/O problem | |
163 | */ | |
164 | public boolean isOpen() throws IOException { | |
165 | 32 | return Issue.OPEN_STATE.equals(this.state()); |
166 | } | |
167 | /** | |
168 | * Open it (make sure it's open). | |
169 | * @throws IOException If there is any I/O problem | |
170 | */ | |
171 | public void open() throws IOException { | |
172 | 2 | this.state(Issue.OPEN_STATE); |
173 | 1 | } |
174 | /** | |
175 | * Close it (make sure it's closed). | |
176 | * @throws IOException If there is any I/O problem | |
177 | */ | |
178 | public void close() throws IOException { | |
179 | 6 | this.state(Issue.CLOSED_STATE); |
180 | 3 | } |
181 | /** | |
182 | * Get its state. | |
183 | * @return State of issue | |
184 | * @throws IOException If there is any I/O problem | |
185 | */ | |
186 | public String state() throws IOException { | |
187 | 32 | return this.jsn.text("state"); |
188 | } | |
189 | /** | |
190 | * Change its state. | |
191 | * @param state State of issue | |
192 | * @throws IOException If there is any I/O problem | |
193 | */ | |
194 | public void state(final String state) throws IOException { | |
195 | 12 | this.issue.patch( |
196 | 4 | Json.createObjectBuilder().add("state", state).build() |
197 | ); | |
198 | 4 | } |
199 | /** | |
200 | * Get its title. | |
201 | * @return Title of issue | |
202 | * @throws IOException If there is any I/O problem | |
203 | */ | |
204 | public String title() throws IOException { | |
205 | 10 | return this.jsn.text("title"); |
206 | } | |
207 | /** | |
208 | * Change its title. | |
209 | * @param text Title of issue | |
210 | * @throws IOException If there is any I/O problem | |
211 | */ | |
212 | public void title(final String text) throws IOException { | |
213 | 3 | this.issue.patch( |
214 | 1 | Json.createObjectBuilder().add("title", text).build() |
215 | ); | |
216 | 1 | } |
217 | /** | |
218 | * Get its body. | |
219 | * @return Body of issue | |
220 | * @throws IOException If there is any I/O problem | |
221 | */ | |
222 | public String body() throws IOException { | |
223 | 4 | return this.jsn.text("body"); |
224 | } | |
225 | /** | |
226 | * Change its body. | |
227 | * @param text Body of issue | |
228 | * @throws IOException If there is any I/O problem | |
229 | */ | |
230 | public void body(final String text) throws IOException { | |
231 | 3 | this.issue.patch( |
232 | 1 | Json.createObjectBuilder().add("body", text).build() |
233 | ); | |
234 | 1 | } |
235 | /** | |
236 | * Has body? | |
237 | * @return TRUE if body exists | |
238 | * @throws IOException If there is any I/O problem | |
239 | * @since 0.22 | |
240 | */ | |
241 | public boolean hasBody() throws IOException { | |
242 | 0 | return this.jsn.hasNotNull("body"); |
243 | } | |
244 | /** | |
245 | * Has assignee? | |
246 | * @return TRUE if assignee exists | |
247 | * @throws IOException If there is any I/O problem | |
248 | */ | |
249 | public boolean hasAssignee() throws IOException { | |
250 | 2 | return this.jsn.hasNotNull("assignee"); |
251 | } | |
252 | /** | |
253 | * Get its assignee. | |
254 | * @return User Assignee of issue | |
255 | * @throws IOException If there is any I/O problem | |
256 | */ | |
257 | public User assignee() throws IOException { | |
258 | 2 | if (!this.hasAssignee()) { |
259 | 0 | throw new IllegalArgumentException( |
260 | 0 | String.format( |
261 | "issue #%d doesn't have an assignee, use hasAssignee()", | |
262 | 0 | this.number() |
263 | ) | |
264 | ); | |
265 | } | |
266 | 2 | return this.issue.repo().github().users().get( |
267 | 1 | this.jsn.value( |
268 | "assignee", JsonObject.class | |
269 | 1 | ).getString("login") |
270 | ); | |
271 | } | |
272 | /** | |
273 | * Assign this issue to another user. | |
274 | * @param login Login of the user to assign to | |
275 | * @throws IOException If there is any I/O problem | |
276 | */ | |
277 | public void assign(final String login) throws IOException { | |
278 | 3 | this.issue.patch( |
279 | 1 | Json.createObjectBuilder().add("assignee", login).build() |
280 | ); | |
281 | 1 | } |
282 | /** | |
283 | * Get its URL. | |
284 | * @return URL of issue | |
285 | * @throws IOException If there is any I/O problem | |
286 | */ | |
287 | public URL url() throws IOException { | |
288 | 0 | return new URL(this.jsn.text("url")); |
289 | } | |
290 | /** | |
291 | * Get its HTML URL. | |
292 | * @return URL of issue | |
293 | * @throws IOException If there is any I/O problem | |
294 | */ | |
295 | public URL htmlUrl() throws IOException { | |
296 | 2 | return new URL(this.jsn.text("html_url")); |
297 | } | |
298 | /** | |
299 | * When this issue was created. | |
300 | * @return Date of creation | |
301 | * @throws IOException If there is any I/O problem | |
302 | */ | |
303 | public Date createdAt() throws IOException { | |
304 | try { | |
305 | 3 | return new Github.Time( |
306 | 1 | this.jsn.text("created_at") |
307 | 1 | ).date(); |
308 | 0 | } catch (final ParseException ex) { |
309 | 0 | throw new IllegalStateException(ex); |
310 | } | |
311 | } | |
312 | /** | |
313 | * When this issue was closed. | |
314 | * @return Date of creation | |
315 | * @throws IOException If there is any I/O problem | |
316 | * @since 0.34 | |
317 | */ | |
318 | public Date closedAt() throws IOException { | |
319 | try { | |
320 | 0 | return new Github.Time( |
321 | 0 | this.jsn.text("closed_at") |
322 | 0 | ).date(); |
323 | 0 | } catch (final ParseException ex) { |
324 | 0 | throw new IllegalStateException(ex); |
325 | } | |
326 | } | |
327 | /** | |
328 | * When this issue was updated. | |
329 | * @return Date of update | |
330 | * @throws IOException If there is any I/O problem | |
331 | */ | |
332 | public Date updatedAt() throws IOException { | |
333 | try { | |
334 | 3 | return new Github.Time( |
335 | 1 | this.jsn.text("updated_at") |
336 | 1 | ).date(); |
337 | 0 | } catch (final ParseException ex) { |
338 | 0 | throw new IllegalStateException(ex); |
339 | } | |
340 | } | |
341 | /** | |
342 | * Is it a pull request? | |
343 | * @return TRUE if it is a pull request | |
344 | * @throws IOException If there is any I/O problem | |
345 | */ | |
346 | public boolean isPull() throws IOException { | |
347 | 15 | return this.json().containsKey("pull_request") |
348 | 4 | && !this.jsn.value("pull_request", JsonObject.class) |
349 | 4 | .isNull("html_url"); |
350 | } | |
351 | ||
352 | /** | |
353 | * Get pull request. | |
354 | * @return Pull request | |
355 | * @throws IOException If there is any I/O problem | |
356 | */ | |
357 | public Pull pull() throws IOException { | |
358 | 2 | final String url = this.jsn.value( |
359 | "pull_request", JsonObject.class | |
360 | 1 | ).getString("html_url"); |
361 | 2 | return this.issue.repo().pulls().get( |
362 | 1 | Integer.parseInt(url.substring(url.lastIndexOf('/') + 1)) |
363 | ); | |
364 | } | |
365 | /** | |
366 | * Get the latest event of a given type. | |
367 | * Throws {@link IllegalStateException} if the issue has no events of | |
368 | * the given type. | |
369 | * @param type Type of event | |
370 | * @return Latest event of the given type | |
371 | * @throws IOException If there is any I/O problem | |
372 | */ | |
373 | public Event latestEvent(final String type) throws IOException { | |
374 | 0 | final Iterable<Event.Smart> events = new Smarts<Event.Smart>( |
375 | 0 | this.issue.events() |
376 | ); | |
377 | 0 | Event found = null; |
378 | 0 | for (final Event.Smart event : events) { |
379 | 0 | if (event.type().equals(type) && (found == null |
380 | 0 | || found.number() < event.number())) { |
381 | 0 | found = event; |
382 | } | |
383 | 0 | } |
384 | 0 | if (found == null) { |
385 | 0 | throw new IllegalStateException( |
386 | 0 | String.format( |
387 | "event of type '%s' not found in issue #%d", | |
388 | 0 | type, this.issue.number() |
389 | ) | |
390 | ); | |
391 | } | |
392 | 0 | return found; |
393 | } | |
394 | /** | |
395 | * Get read-only labels. | |
396 | * @return Collection of labels | |
397 | * @throws IOException If there is any I/O problem | |
398 | * @since 0.6.2 | |
399 | */ | |
400 | public IssueLabels roLabels() throws IOException { | |
401 | 4 | final Collection<JsonObject> array = |
402 | 2 | this.jsn.value("labels", JsonArray.class) |
403 | 2 | .getValuesAs(JsonObject.class); |
404 | 2 | final Collection<Label> labels = new ArrayList<Label>(array.size()); |
405 | 2 | for (final JsonObject obj : array) { |
406 | 4 | labels.add( |
407 | new Label.Unmodified( | |
408 | 2 | Issue.Smart.this.repo(), |
409 | 2 | obj.toString() |
410 | ) | |
411 | ); | |
412 | 2 | } |
413 | // @checkstyle AnonInnerLength (1 line) | |
414 | 2 | return new IssueLabels() { |
415 | @Override | |
416 | public Issue issue() { | |
417 | 0 | return Issue.Smart.this; |
418 | } | |
419 | @Override | |
420 | public void add( | |
421 | final Iterable<String> labels) throws IOException { | |
422 | 1 | throw new UnsupportedOperationException( |
423 | "The issue is read-only." | |
424 | ); | |
425 | } | |
426 | @Override | |
427 | public void replace( | |
428 | final Iterable<String> labels) throws IOException { | |
429 | 0 | throw new UnsupportedOperationException( |
430 | "The issue is read-only." | |
431 | ); | |
432 | } | |
433 | @Override | |
434 | public Iterable<Label> iterate() { | |
435 | 1 | return labels; |
436 | } | |
437 | @Override | |
438 | public void remove( | |
439 | final String name) throws IOException { | |
440 | 0 | throw new UnsupportedOperationException( |
441 | "This issue is read-only." | |
442 | ); | |
443 | } | |
444 | @Override | |
445 | public void clear() throws IOException { | |
446 | 0 | throw new UnsupportedOperationException( |
447 | "This issue is read-only." | |
448 | ); | |
449 | } | |
450 | }; | |
451 | } | |
452 | /** | |
453 | * Does issue have milestone? | |
454 | * @return True if has | |
455 | * @throws IOException If fails | |
456 | */ | |
457 | public boolean hasMilestone() throws IOException { | |
458 | 0 | return this.jsn.hasNotNull("milestone"); |
459 | } | |
460 | /** | |
461 | * Get milestone for this issue. | |
462 | * @return Milestone | |
463 | * @throws IOException If fails | |
464 | */ | |
465 | public Milestone milestone() throws IOException { | |
466 | 0 | return this.repo().milestones().get( |
467 | 0 | this.jsn.value("milestone", JsonObject.class) |
468 | 0 | .getInt("number") |
469 | ); | |
470 | } | |
471 | /** | |
472 | * Add issueto milestone. | |
473 | * @param milestone Milestone | |
474 | * @throws IOException If fails | |
475 | */ | |
476 | public void milestone(final Milestone milestone) throws IOException { | |
477 | 0 | this.patch( |
478 | 0 | Json.createObjectBuilder().add( |
479 | 0 | "milestone", milestone.number() |
480 | 0 | ).build() |
481 | ); | |
482 | 0 | } |
483 | @Override | |
484 | public Repo repo() { | |
485 | 4 | return this.issue.repo(); |
486 | } | |
487 | @Override | |
488 | public int number() { | |
489 | 0 | return this.issue.number(); |
490 | } | |
491 | @Override | |
492 | public Comments comments() { | |
493 | 0 | return this.issue.comments(); |
494 | } | |
495 | @Override | |
496 | public IssueLabels labels() { | |
497 | 0 | return this.issue.labels(); |
498 | } | |
499 | @Override | |
500 | public Iterable<Event> events() throws IOException { | |
501 | 8 | return this.issue.events(); |
502 | } | |
503 | @Override | |
504 | public JsonObject json() throws IOException { | |
505 | 10 | return this.issue.json(); |
506 | } | |
507 | @Override | |
508 | public void patch(final JsonObject json) throws IOException { | |
509 | 0 | this.issue.patch(json); |
510 | 0 | } |
511 | @Override | |
512 | public int compareTo(final Issue obj) { | |
513 | 0 | return this.issue.compareTo(obj); |
514 | } | |
515 | @Override | |
516 | public boolean exists() throws IOException { | |
517 | 0 | return new Existence(this.issue).check(); |
518 | } | |
519 | } | |
520 | ||
521 | } |