001/*
002 * SonarQube, open source software quality management tool.
003 * Copyright (C) 2008-2013 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.Maps;
028import org.apache.commons.lang.StringUtils;
029import org.apache.commons.lang.builder.ToStringBuilder;
030import org.apache.commons.lang.builder.ToStringStyle;
031import org.apache.commons.lang.time.DateUtils;
032import org.sonar.api.issue.Issue;
033import org.sonar.api.issue.IssueComment;
034import org.sonar.api.rule.RuleKey;
035import org.sonar.api.rule.Severity;
036import org.sonar.api.utils.internal.WorkDuration;
037
038import javax.annotation.CheckForNull;
039import javax.annotation.Nullable;
040
041import java.io.Serializable;
042import java.util.*;
043
044import static com.google.common.collect.Lists.newArrayList;
045
046/**
047 * PLUGINS MUST NOT BE USED THIS CLASS, EXCEPT FOR UNIT TESTING.
048 *
049 * @since 3.6
050 */
051public class DefaultIssue implements Issue {
052
053  private String key;
054  private String componentKey;
055  private Long componentId;
056  private String projectKey;
057  private RuleKey ruleKey;
058  private String severity;
059  private boolean manualSeverity = false;
060  private String message;
061  private Integer line;
062  private Double effortToFix;
063  private WorkDuration technicalDebt;
064  private String status;
065  private String resolution;
066  private String reporter;
067  private String assignee;
068  private String checksum;
069  private Map<String, String> attributes = null;
070  private String authorLogin = null;
071  private String actionPlanKey;
072  private List<IssueComment> comments = null;
073
074  // FUNCTIONAL DATES
075  private Date creationDate;
076  private Date updateDate;
077  private Date closeDate;
078
079  // FOLLOWING FIELDS ARE AVAILABLE ONLY DURING SCAN
080
081  // Current changes
082  private FieldDiffs currentChange = null;
083
084  // all changes
085  private List<FieldDiffs> changes = null;
086
087  // true if the the issue did not exist in the previous scan.
088  private boolean isNew = true;
089
090  // True if the the issue did exist in the previous scan but not in the current one. That means
091  // that this issue should be closed.
092  private boolean endOfLife = false;
093
094  private boolean onDisabledRule = false;
095
096  // true if some fields have been changed since the previous scan
097  private boolean isChanged = false;
098
099  // true if notifications have to be sent
100  private boolean sendNotifications = false;
101
102  // Date when issue was loaded from db (only when isNew=false)
103  private Date selectedAt;
104
105  public String key() {
106    return key;
107  }
108
109  public DefaultIssue setKey(String key) {
110    this.key = key;
111    return this;
112  }
113
114  public String componentKey() {
115    return componentKey;
116  }
117
118  public DefaultIssue setComponentKey(String s) {
119    this.componentKey = s;
120    return this;
121  }
122
123  /**
124   * The component id not populated on batch side
125   */
126  @CheckForNull
127  public Long componentId() {
128    return componentId;
129  }
130
131  public DefaultIssue setComponentId(@Nullable Long s) {
132    this.componentId = s;
133    return this;
134  }
135
136  /**
137   * The project key is not always populated, that's why it's not present in the Issue API
138   */
139  @CheckForNull
140  public String projectKey() {
141    return projectKey;
142  }
143
144  public DefaultIssue setProjectKey(String projectKey) {
145    this.projectKey = projectKey;
146    return this;
147  }
148
149  public RuleKey ruleKey() {
150    return ruleKey;
151  }
152
153  public DefaultIssue setRuleKey(RuleKey k) {
154    this.ruleKey = k;
155    return this;
156  }
157
158  public String severity() {
159    return severity;
160  }
161
162  public DefaultIssue setSeverity(@Nullable String s) {
163    Preconditions.checkArgument(s == null || Severity.ALL.contains(s), "Not a valid severity: " + s);
164    this.severity = s;
165    return this;
166  }
167
168  public boolean manualSeverity() {
169    return manualSeverity;
170  }
171
172  public DefaultIssue setManualSeverity(boolean b) {
173    this.manualSeverity = b;
174    return this;
175  }
176
177  @CheckForNull
178  public String message() {
179    return message;
180  }
181
182  public DefaultIssue setMessage(@Nullable String s) {
183    this.message = StringUtils.abbreviate(StringUtils.trim(s), MESSAGE_MAX_SIZE);
184    return this;
185  }
186
187  @CheckForNull
188  public Integer line() {
189    return line;
190  }
191
192  public DefaultIssue setLine(@Nullable Integer l) {
193    Preconditions.checkArgument(l == null || l > 0, "Line must be null or greater than zero (got " + l + ")");
194    this.line = l;
195    return this;
196  }
197
198  @CheckForNull
199  public Double effortToFix() {
200    return effortToFix;
201  }
202
203  public DefaultIssue setEffortToFix(@Nullable Double d) {
204    Preconditions.checkArgument(d == null || d >= 0, "Effort to fix must be greater than or equal 0 (got " + d + ")");
205    this.effortToFix = d;
206    return this;
207  }
208
209  /**
210   * Elapsed time to fix the issue
211   */
212  @CheckForNull
213  public WorkDuration technicalDebt() {
214    return technicalDebt;
215  }
216
217  public DefaultIssue setTechnicalDebt(@Nullable WorkDuration t) {
218    this.technicalDebt = t;
219    return this;
220  }
221
222  public String status() {
223    return status;
224  }
225
226  public DefaultIssue setStatus(String s) {
227    Preconditions.checkArgument(!Strings.isNullOrEmpty(s), "Status must be set");
228    this.status = s;
229    return this;
230  }
231
232  @CheckForNull
233  public String resolution() {
234    return resolution;
235  }
236
237  public DefaultIssue setResolution(@Nullable String s) {
238    this.resolution = s;
239    return this;
240  }
241
242  @CheckForNull
243  public String reporter() {
244    return reporter;
245  }
246
247  public DefaultIssue setReporter(@Nullable String s) {
248    this.reporter = s;
249    return this;
250  }
251
252  @CheckForNull
253  public String assignee() {
254    return assignee;
255  }
256
257  public DefaultIssue setAssignee(@Nullable String s) {
258    this.assignee = s;
259    return this;
260  }
261
262  public Date creationDate() {
263    return creationDate;
264  }
265
266  public DefaultIssue setCreationDate(Date d) {
267    // d is not marked as Nullable but we still allow null parameter for unit testing.
268    this.creationDate = (d != null ? DateUtils.truncate(d, Calendar.SECOND) : null);
269    return this;
270  }
271
272  @CheckForNull
273  public Date updateDate() {
274    return updateDate;
275  }
276
277  public DefaultIssue setUpdateDate(@Nullable Date d) {
278    this.updateDate = (d != null ? DateUtils.truncate(d, Calendar.SECOND) : null);
279    return this;
280  }
281
282  @CheckForNull
283  public Date closeDate() {
284    return closeDate;
285  }
286
287  public DefaultIssue setCloseDate(@Nullable Date d) {
288    this.closeDate = (d != null ? DateUtils.truncate(d, Calendar.SECOND) : null);
289    return this;
290  }
291
292  @CheckForNull
293  public String checksum() {
294    return checksum;
295  }
296
297  public DefaultIssue setChecksum(@Nullable String s) {
298    this.checksum = s;
299    return this;
300  }
301
302  @Override
303  public boolean isNew() {
304    return isNew;
305  }
306
307  public DefaultIssue setNew(boolean b) {
308    isNew = b;
309    return this;
310  }
311
312  /**
313   * True when one of the following conditions is true :
314   * <ul>
315   * <li>the related component has been deleted or renamed</li>
316   * <li>the rule has been deleted (eg. on plugin uninstall)</li>
317   * <li>the rule has been disabled in the Quality profile</li>
318   * </ul>
319   */
320  public boolean isEndOfLife() {
321    return endOfLife;
322  }
323
324  public DefaultIssue setEndOfLife(boolean b) {
325    endOfLife = b;
326    return this;
327  }
328
329  public boolean isOnDisabledRule() {
330    return onDisabledRule;
331  }
332
333  public DefaultIssue setOnDisabledRule(boolean b) {
334    onDisabledRule = b;
335    return this;
336  }
337
338  public boolean isChanged() {
339    return isChanged;
340  }
341
342  public DefaultIssue setChanged(boolean b) {
343    isChanged = b;
344    return this;
345  }
346
347  public boolean mustSendNotifications() {
348    return sendNotifications;
349  }
350
351  public DefaultIssue setSendNotifications(boolean b) {
352    sendNotifications = b;
353    return this;
354  }
355
356  @CheckForNull
357  public String attribute(String key) {
358    return attributes == null ? null : attributes.get(key);
359  }
360
361  public DefaultIssue setAttribute(String key, @Nullable String value) {
362    if (attributes == null) {
363      attributes = Maps.newHashMap();
364    }
365    if (value == null) {
366      attributes.remove(key);
367    } else {
368      attributes.put(key, value);
369    }
370    return this;
371  }
372
373  public Map<String, String> attributes() {
374    return attributes == null ? Collections.<String, String>emptyMap() : ImmutableMap.copyOf(attributes);
375  }
376
377  public DefaultIssue setAttributes(@Nullable Map<String, String> map) {
378    if (map != null) {
379      if (attributes == null) {
380        attributes = Maps.newHashMap();
381      }
382      attributes.putAll(map);
383    }
384    return this;
385  }
386
387  @CheckForNull
388  public String authorLogin() {
389    return authorLogin;
390  }
391
392  public DefaultIssue setAuthorLogin(@Nullable String s) {
393    this.authorLogin = s;
394    return this;
395  }
396
397  @CheckForNull
398  public String actionPlanKey() {
399    return actionPlanKey;
400  }
401
402  public DefaultIssue setActionPlanKey(@Nullable String actionPlanKey) {
403    this.actionPlanKey = actionPlanKey;
404    return this;
405  }
406
407  public DefaultIssue setFieldChange(IssueChangeContext context, String field, @Nullable Serializable oldValue, @Nullable Serializable newValue) {
408    if (!Objects.equal(oldValue, newValue)) {
409      if (currentChange == null) {
410        currentChange = new FieldDiffs();
411        currentChange.setUserLogin(context.login());
412        currentChange.setCreationDate(context.date());
413      }
414      currentChange.setDiff(field, oldValue, newValue);
415    }
416    addChange(currentChange);
417    return this;
418  }
419
420  @CheckForNull
421  public FieldDiffs currentChange() {
422    return currentChange;
423  }
424
425  public DefaultIssue addChange(FieldDiffs change) {
426    if (changes == null) {
427      changes = newArrayList();
428    }
429    changes.add(change);
430    return this;
431  }
432
433  public DefaultIssue setChanges(List<FieldDiffs> changes) {
434    this.changes = changes;
435    return this;
436  }
437
438  public List<FieldDiffs> changes() {
439    if (changes == null) {
440      return Collections.emptyList();
441    }
442    return ImmutableList.copyOf(changes);
443  }
444
445  public DefaultIssue addComment(DefaultIssueComment comment) {
446    if (comments == null) {
447      comments = newArrayList();
448    }
449    comments.add(comment);
450    return this;
451  }
452
453  @SuppressWarnings("unchcked")
454  public List<IssueComment> comments() {
455    if (comments == null) {
456      return Collections.emptyList();
457    }
458    return ImmutableList.copyOf(comments);
459  }
460
461  @CheckForNull
462  public Date selectedAt() {
463    return selectedAt;
464  }
465
466  public DefaultIssue setSelectedAt(@Nullable Date d) {
467    this.selectedAt = d;
468    return this;
469  }
470
471  @Override
472  public boolean equals(Object o) {
473    if (this == o) {
474      return true;
475    }
476    if (o == null || getClass() != o.getClass()) {
477      return false;
478    }
479    DefaultIssue that = (DefaultIssue) o;
480    if (key != null ? !key.equals(that.key) : that.key != null) {
481      return false;
482    }
483    return true;
484  }
485
486  @Override
487  public int hashCode() {
488    return key != null ? key.hashCode() : 0;
489  }
490
491  @Override
492  public String toString() {
493    return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
494  }
495
496}