001/*
002 * SonarQube, open source software quality management tool.
003 * Copyright (C) 2008-2014 SonarSource
004 * mailto:contact AT sonarsource DOT com
005 *
006 * SonarQube is free software; you can redistribute it and/or
007 * modify it under the terms of the GNU Lesser General Public
008 * License as published by the Free Software Foundation; either
009 * version 3 of the License, or (at your option) any later version.
010 *
011 * SonarQube is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014 * Lesser General Public License for more details.
015 *
016 * You should have received a copy of the GNU Lesser General Public License
017 * along with this program; if not, write to the Free Software Foundation,
018 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
019 */
020package org.sonar.api.issue.internal;
021
022import com.google.common.base.Objects;
023import com.google.common.base.Preconditions;
024import com.google.common.base.Strings;
025import com.google.common.collect.ImmutableList;
026import com.google.common.collect.ImmutableMap;
027import com.google.common.collect.ImmutableSet;
028import com.google.common.collect.Maps;
029import org.apache.commons.lang.StringUtils;
030import org.apache.commons.lang.builder.ToStringBuilder;
031import org.apache.commons.lang.builder.ToStringStyle;
032import org.apache.commons.lang.time.DateUtils;
033import org.sonar.api.issue.Issue;
034import org.sonar.api.issue.IssueComment;
035import org.sonar.api.rule.RuleKey;
036import org.sonar.api.rule.Severity;
037import org.sonar.api.utils.Duration;
038
039import javax.annotation.CheckForNull;
040import javax.annotation.Nullable;
041
042import java.io.Serializable;
043import java.util.*;
044
045import static com.google.common.collect.Lists.newArrayList;
046
047/**
048 * PLUGINS MUST NOT BE USED THIS CLASS, EXCEPT FOR UNIT TESTING.
049 *
050 * @since 3.6
051 */
052public class DefaultIssue implements Issue {
053
054  private String key;
055
056  private String componentUuid;
057  private String componentKey;
058
059  private String moduleUuid;
060  private String moduleUuidPath;
061
062  private String projectUuid;
063  private String projectKey;
064
065  private RuleKey ruleKey;
066  private String language;
067  private String severity;
068  private boolean manualSeverity = false;
069  private String message;
070  private Integer line;
071  private Double effortToFix;
072  private Duration debt;
073  private String status;
074  private String resolution;
075  private String reporter;
076  private String assignee;
077  private String checksum;
078  private Map<String, String> attributes = null;
079  private String authorLogin = null;
080  private String actionPlanKey;
081  private List<IssueComment> comments = null;
082  private Set<String> tags = null;
083
084  // FUNCTIONAL DATES
085  private Date creationDate;
086  private Date updateDate;
087  private Date closeDate;
088
089  // FOLLOWING FIELDS ARE AVAILABLE ONLY DURING SCAN
090
091  // Current changes
092  private FieldDiffs currentChange = null;
093
094  // all changes
095  private List<FieldDiffs> changes = null;
096
097  // true if the the issue did not exist in the previous scan.
098  private boolean isNew = true;
099
100  // True if the the issue did exist in the previous scan but not in the current one. That means
101  // that this issue should be closed.
102  private boolean endOfLife = false;
103
104  private boolean onDisabledRule = false;
105
106  // true if some fields have been changed since the previous scan
107  private boolean isChanged = false;
108
109  // true if notifications have to be sent
110  private boolean sendNotifications = false;
111
112  // Date when issue was loaded from db (only when isNew=false)
113  private Long selectedAt;
114
115  @Override
116  public String key() {
117    return key;
118  }
119
120  public DefaultIssue setKey(String key) {
121    this.key = key;
122    return this;
123  }
124
125  /**
126   * Can be null on Views or Devs
127   */
128  @Override
129  @CheckForNull
130  public String componentUuid() {
131    return componentUuid;
132  }
133
134  public DefaultIssue setComponentUuid(@CheckForNull String componentUuid) {
135    this.componentUuid = componentUuid;
136    return this;
137  }
138
139  @Override
140  public String componentKey() {
141    return componentKey;
142  }
143
144  public DefaultIssue setComponentKey(String s) {
145    this.componentKey = s;
146    return this;
147  }
148
149  @CheckForNull
150  public String moduleUuid() {
151    return moduleUuid;
152  }
153
154  public DefaultIssue setModuleUuid(@Nullable String moduleUuid) {
155    this.moduleUuid = moduleUuid;
156    return this;
157  }
158
159  @CheckForNull
160  public String moduleUuidPath() {
161    return moduleUuidPath;
162  }
163
164  public DefaultIssue setModuleUuidPath(@Nullable String moduleUuidPath) {
165    this.moduleUuidPath = moduleUuidPath;
166    return this;
167  }
168
169  /**
170   * Can be null on Views or Devs
171   */
172  @Override
173  @CheckForNull
174  public String projectUuid() {
175    return projectUuid;
176  }
177
178  public DefaultIssue setProjectUuid(@Nullable String projectUuid) {
179    this.projectUuid = projectUuid;
180    return this;
181  }
182
183  @Override
184  public String projectKey() {
185    return projectKey;
186  }
187
188  public DefaultIssue setProjectKey(String projectKey) {
189    this.projectKey = projectKey;
190    return this;
191  }
192
193  @Override
194  public RuleKey ruleKey() {
195    return ruleKey;
196  }
197
198  public DefaultIssue setRuleKey(RuleKey k) {
199    this.ruleKey = k;
200    return this;
201  }
202
203  @Override
204  public String language() {
205    return language;
206  }
207
208  public DefaultIssue setLanguage(String l) {
209    this.language = l;
210    return this;
211  }
212
213  @Override
214  public String severity() {
215    return severity;
216  }
217
218  public DefaultIssue setSeverity(@Nullable String s) {
219    Preconditions.checkArgument(s == null || Severity.ALL.contains(s), "Not a valid severity: " + s);
220    this.severity = s;
221    return this;
222  }
223
224  public boolean manualSeverity() {
225    return manualSeverity;
226  }
227
228  public DefaultIssue setManualSeverity(boolean b) {
229    this.manualSeverity = b;
230    return this;
231  }
232
233  @Override
234  @CheckForNull
235  public String message() {
236    return message;
237  }
238
239  public DefaultIssue setMessage(@Nullable String s) {
240    this.message = StringUtils.abbreviate(StringUtils.trim(s), MESSAGE_MAX_SIZE);
241    return this;
242  }
243
244  @Override
245  @CheckForNull
246  public Integer line() {
247    return line;
248  }
249
250  public DefaultIssue setLine(@Nullable Integer l) {
251    Preconditions.checkArgument(l == null || l > 0, "Line must be null or greater than zero (got " + l + ")");
252    this.line = l;
253    return this;
254  }
255
256  @Override
257  @CheckForNull
258  public Double effortToFix() {
259    return effortToFix;
260  }
261
262  public DefaultIssue setEffortToFix(@Nullable Double d) {
263    Preconditions.checkArgument(d == null || d >= 0, "Effort to fix must be greater than or equal 0 (got " + d + ")");
264    this.effortToFix = d;
265    return this;
266  }
267
268  /**
269   * Elapsed time to fix the issue
270   */
271  @Override
272  @CheckForNull
273  public Duration debt() {
274    return debt;
275  }
276
277  @CheckForNull
278  public Long debtInMinutes() {
279    return debt != null ? debt.toMinutes() : null;
280  }
281
282  public DefaultIssue setDebt(@Nullable Duration t) {
283    this.debt = t;
284    return this;
285  }
286
287  @Override
288  public String status() {
289    return status;
290  }
291
292  public DefaultIssue setStatus(String s) {
293    Preconditions.checkArgument(!Strings.isNullOrEmpty(s), "Status must be set");
294    this.status = s;
295    return this;
296  }
297
298  @Override
299  @CheckForNull
300  public String resolution() {
301    return resolution;
302  }
303
304  public DefaultIssue setResolution(@Nullable String s) {
305    this.resolution = s;
306    return this;
307  }
308
309  @Override
310  @CheckForNull
311  public String reporter() {
312    return reporter;
313  }
314
315  public DefaultIssue setReporter(@Nullable String s) {
316    this.reporter = s;
317    return this;
318  }
319
320  @Override
321  @CheckForNull
322  public String assignee() {
323    return assignee;
324  }
325
326  public DefaultIssue setAssignee(@Nullable String s) {
327    this.assignee = s;
328    return this;
329  }
330
331  @Override
332  public Date creationDate() {
333    return creationDate;
334  }
335
336  public DefaultIssue setCreationDate(Date d) {
337    // d is not marked as Nullable but we still allow null parameter for unit testing.
338    this.creationDate = (d != null ? DateUtils.truncate(d, Calendar.SECOND) : null);
339    return this;
340  }
341
342  @Override
343  @CheckForNull
344  public Date updateDate() {
345    return updateDate;
346  }
347
348  public DefaultIssue setUpdateDate(@Nullable Date d) {
349    this.updateDate = (d != null ? DateUtils.truncate(d, Calendar.SECOND) : null);
350    return this;
351  }
352
353  @Override
354  @CheckForNull
355  public Date closeDate() {
356    return closeDate;
357  }
358
359  public DefaultIssue setCloseDate(@Nullable Date d) {
360    this.closeDate = (d != null ? DateUtils.truncate(d, Calendar.SECOND) : null);
361    return this;
362  }
363
364  @CheckForNull
365  public String checksum() {
366    return checksum;
367  }
368
369  public DefaultIssue setChecksum(@Nullable String s) {
370    this.checksum = s;
371    return this;
372  }
373
374  @Override
375  public boolean isNew() {
376    return isNew;
377  }
378
379  public DefaultIssue setNew(boolean b) {
380    isNew = b;
381    return this;
382  }
383
384  /**
385   * True when one of the following conditions is true :
386   * <ul>
387   * <li>the related component has been deleted or renamed</li>
388   * <li>the rule has been deleted (eg. on plugin uninstall)</li>
389   * <li>the rule has been disabled in the Quality profile</li>
390   * </ul>
391   */
392  public boolean isEndOfLife() {
393    return endOfLife;
394  }
395
396  public DefaultIssue setEndOfLife(boolean b) {
397    endOfLife = b;
398    return this;
399  }
400
401  public boolean isOnDisabledRule() {
402    return onDisabledRule;
403  }
404
405  public DefaultIssue setOnDisabledRule(boolean b) {
406    onDisabledRule = b;
407    return this;
408  }
409
410  public boolean isChanged() {
411    return isChanged;
412  }
413
414  public DefaultIssue setChanged(boolean b) {
415    isChanged = b;
416    return this;
417  }
418
419  public boolean mustSendNotifications() {
420    return sendNotifications;
421  }
422
423  public DefaultIssue setSendNotifications(boolean b) {
424    sendNotifications = b;
425    return this;
426  }
427
428  @Override
429  @CheckForNull
430  public String attribute(String key) {
431    return attributes == null ? null : attributes.get(key);
432  }
433
434  public DefaultIssue setAttribute(String key, @Nullable String value) {
435    if (attributes == null) {
436      attributes = Maps.newHashMap();
437    }
438    if (value == null) {
439      attributes.remove(key);
440    } else {
441      attributes.put(key, value);
442    }
443    return this;
444  }
445
446  @Override
447  public Map<String, String> attributes() {
448    return attributes == null ? Collections.<String, String>emptyMap() : ImmutableMap.copyOf(attributes);
449  }
450
451  public DefaultIssue setAttributes(@Nullable Map<String, String> map) {
452    if (map != null) {
453      if (attributes == null) {
454        attributes = Maps.newHashMap();
455      }
456      attributes.putAll(map);
457    }
458    return this;
459  }
460
461  @Override
462  @CheckForNull
463  public String authorLogin() {
464    return authorLogin;
465  }
466
467  public DefaultIssue setAuthorLogin(@Nullable String s) {
468    this.authorLogin = s;
469    return this;
470  }
471
472  @Override
473  @CheckForNull
474  public String actionPlanKey() {
475    return actionPlanKey;
476  }
477
478  public DefaultIssue setActionPlanKey(@Nullable String actionPlanKey) {
479    this.actionPlanKey = actionPlanKey;
480    return this;
481  }
482
483  public DefaultIssue setFieldChange(IssueChangeContext context, String field, @Nullable Serializable oldValue, @Nullable Serializable newValue) {
484    if (!Objects.equal(oldValue, newValue)) {
485      if (currentChange == null) {
486        currentChange = new FieldDiffs();
487        currentChange.setUserLogin(context.login());
488        currentChange.setCreationDate(context.date());
489      }
490      currentChange.setDiff(field, oldValue, newValue);
491    }
492    addChange(currentChange);
493    return this;
494  }
495
496  public DefaultIssue setCurrentChange(FieldDiffs currentChange) {
497    this.currentChange = currentChange;
498    addChange(currentChange);
499    return this;
500  }
501
502  @CheckForNull
503  public FieldDiffs currentChange() {
504    return currentChange;
505  }
506
507  public DefaultIssue addChange(FieldDiffs change) {
508    if (changes == null) {
509      changes = newArrayList();
510    }
511    changes.add(change);
512    return this;
513  }
514
515  public DefaultIssue setChanges(List<FieldDiffs> changes) {
516    this.changes = changes;
517    return this;
518  }
519
520  public List<FieldDiffs> changes() {
521    if (changes == null) {
522      return Collections.emptyList();
523    }
524    return ImmutableList.copyOf(changes);
525  }
526
527  public DefaultIssue addComment(DefaultIssueComment comment) {
528    if (comments == null) {
529      comments = newArrayList();
530    }
531    comments.add(comment);
532    return this;
533  }
534
535  @Override
536  public List<IssueComment> comments() {
537    if (comments == null) {
538      return Collections.emptyList();
539    }
540    return ImmutableList.copyOf(comments);
541  }
542
543  @CheckForNull
544  public Long selectedAt() {
545    return selectedAt;
546  }
547
548  public DefaultIssue setSelectedAt(@Nullable Long d) {
549    this.selectedAt = d;
550    return this;
551  }
552
553  @Override
554  public boolean equals(Object o) {
555    if (this == o) {
556      return true;
557    }
558    if (o == null || getClass() != o.getClass()) {
559      return false;
560    }
561    DefaultIssue that = (DefaultIssue) o;
562    if (key != null ? !key.equals(that.key) : that.key != null) {
563      return false;
564    }
565    return true;
566  }
567
568  @Override
569  public int hashCode() {
570    return key != null ? key.hashCode() : 0;
571  }
572
573  @Override
574  public String toString() {
575    return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
576  }
577
578  @Override
579  public Collection<String> tags() {
580    if (tags == null) {
581      return ImmutableSet.of();
582    } else {
583      return ImmutableSet.copyOf(tags);
584    }
585  }
586
587  public DefaultIssue setTags(Collection<String> tags) {
588    this.tags = new LinkedHashSet<>(tags);
589    return this;
590  }
591}