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