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 */
020 package org.sonar.api.issue.internal;
021
022 import com.google.common.base.Objects;
023 import com.google.common.base.Preconditions;
024 import com.google.common.base.Strings;
025 import com.google.common.collect.ImmutableList;
026 import com.google.common.collect.ImmutableMap;
027 import com.google.common.collect.Maps;
028 import org.apache.commons.lang.StringUtils;
029 import org.apache.commons.lang.builder.ToStringBuilder;
030 import org.apache.commons.lang.builder.ToStringStyle;
031 import org.apache.commons.lang.time.DateUtils;
032 import org.sonar.api.issue.Issue;
033 import org.sonar.api.issue.IssueComment;
034 import org.sonar.api.rule.RuleKey;
035 import org.sonar.api.rule.Severity;
036 import org.sonar.api.utils.Duration;
037
038 import javax.annotation.CheckForNull;
039 import javax.annotation.Nullable;
040
041 import java.io.Serializable;
042 import java.util.*;
043
044 import 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 */
051 public 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 Duration debt;
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 Duration debt() {
214 return debt;
215 }
216
217 @CheckForNull
218 public Long debtInMinutes(){
219 return debt != null ? debt.toMinutes() : null;
220 }
221
222 public DefaultIssue setDebt(@Nullable Duration t) {
223 this.debt = t;
224 return this;
225 }
226
227 public String status() {
228 return status;
229 }
230
231 public DefaultIssue setStatus(String s) {
232 Preconditions.checkArgument(!Strings.isNullOrEmpty(s), "Status must be set");
233 this.status = s;
234 return this;
235 }
236
237 @CheckForNull
238 public String resolution() {
239 return resolution;
240 }
241
242 public DefaultIssue setResolution(@Nullable String s) {
243 this.resolution = s;
244 return this;
245 }
246
247 @CheckForNull
248 public String reporter() {
249 return reporter;
250 }
251
252 public DefaultIssue setReporter(@Nullable String s) {
253 this.reporter = s;
254 return this;
255 }
256
257 @CheckForNull
258 public String assignee() {
259 return assignee;
260 }
261
262 public DefaultIssue setAssignee(@Nullable String s) {
263 this.assignee = s;
264 return this;
265 }
266
267 public Date creationDate() {
268 return creationDate;
269 }
270
271 public DefaultIssue setCreationDate(Date d) {
272 // d is not marked as Nullable but we still allow null parameter for unit testing.
273 this.creationDate = (d != null ? DateUtils.truncate(d, Calendar.SECOND) : null);
274 return this;
275 }
276
277 @CheckForNull
278 public Date updateDate() {
279 return updateDate;
280 }
281
282 public DefaultIssue setUpdateDate(@Nullable Date d) {
283 this.updateDate = (d != null ? DateUtils.truncate(d, Calendar.SECOND) : null);
284 return this;
285 }
286
287 @CheckForNull
288 public Date closeDate() {
289 return closeDate;
290 }
291
292 public DefaultIssue setCloseDate(@Nullable Date d) {
293 this.closeDate = (d != null ? DateUtils.truncate(d, Calendar.SECOND) : null);
294 return this;
295 }
296
297 @CheckForNull
298 public String checksum() {
299 return checksum;
300 }
301
302 public DefaultIssue setChecksum(@Nullable String s) {
303 this.checksum = s;
304 return this;
305 }
306
307 @Override
308 public boolean isNew() {
309 return isNew;
310 }
311
312 public DefaultIssue setNew(boolean b) {
313 isNew = b;
314 return this;
315 }
316
317 /**
318 * True when one of the following conditions is true :
319 * <ul>
320 * <li>the related component has been deleted or renamed</li>
321 * <li>the rule has been deleted (eg. on plugin uninstall)</li>
322 * <li>the rule has been disabled in the Quality profile</li>
323 * </ul>
324 */
325 public boolean isEndOfLife() {
326 return endOfLife;
327 }
328
329 public DefaultIssue setEndOfLife(boolean b) {
330 endOfLife = b;
331 return this;
332 }
333
334 public boolean isOnDisabledRule() {
335 return onDisabledRule;
336 }
337
338 public DefaultIssue setOnDisabledRule(boolean b) {
339 onDisabledRule = b;
340 return this;
341 }
342
343 public boolean isChanged() {
344 return isChanged;
345 }
346
347 public DefaultIssue setChanged(boolean b) {
348 isChanged = b;
349 return this;
350 }
351
352 public boolean mustSendNotifications() {
353 return sendNotifications;
354 }
355
356 public DefaultIssue setSendNotifications(boolean b) {
357 sendNotifications = b;
358 return this;
359 }
360
361 @CheckForNull
362 public String attribute(String key) {
363 return attributes == null ? null : attributes.get(key);
364 }
365
366 public DefaultIssue setAttribute(String key, @Nullable String value) {
367 if (attributes == null) {
368 attributes = Maps.newHashMap();
369 }
370 if (value == null) {
371 attributes.remove(key);
372 } else {
373 attributes.put(key, value);
374 }
375 return this;
376 }
377
378 public Map<String, String> attributes() {
379 return attributes == null ? Collections.<String, String>emptyMap() : ImmutableMap.copyOf(attributes);
380 }
381
382 public DefaultIssue setAttributes(@Nullable Map<String, String> map) {
383 if (map != null) {
384 if (attributes == null) {
385 attributes = Maps.newHashMap();
386 }
387 attributes.putAll(map);
388 }
389 return this;
390 }
391
392 @CheckForNull
393 public String authorLogin() {
394 return authorLogin;
395 }
396
397 public DefaultIssue setAuthorLogin(@Nullable String s) {
398 this.authorLogin = s;
399 return this;
400 }
401
402 @CheckForNull
403 public String actionPlanKey() {
404 return actionPlanKey;
405 }
406
407 public DefaultIssue setActionPlanKey(@Nullable String actionPlanKey) {
408 this.actionPlanKey = actionPlanKey;
409 return this;
410 }
411
412 public DefaultIssue setFieldChange(IssueChangeContext context, String field, @Nullable Serializable oldValue, @Nullable Serializable newValue) {
413 if (!Objects.equal(oldValue, newValue)) {
414 if (currentChange == null) {
415 currentChange = new FieldDiffs();
416 currentChange.setUserLogin(context.login());
417 currentChange.setCreationDate(context.date());
418 }
419 currentChange.setDiff(field, oldValue, newValue);
420 }
421 addChange(currentChange);
422 return this;
423 }
424
425 @CheckForNull
426 public FieldDiffs currentChange() {
427 return currentChange;
428 }
429
430 public DefaultIssue addChange(FieldDiffs change) {
431 if (changes == null) {
432 changes = newArrayList();
433 }
434 changes.add(change);
435 return this;
436 }
437
438 public DefaultIssue setChanges(List<FieldDiffs> changes) {
439 this.changes = changes;
440 return this;
441 }
442
443 public List<FieldDiffs> changes() {
444 if (changes == null) {
445 return Collections.emptyList();
446 }
447 return ImmutableList.copyOf(changes);
448 }
449
450 public DefaultIssue addComment(DefaultIssueComment comment) {
451 if (comments == null) {
452 comments = newArrayList();
453 }
454 comments.add(comment);
455 return this;
456 }
457
458 @SuppressWarnings("unchcked")
459 public List<IssueComment> comments() {
460 if (comments == null) {
461 return Collections.emptyList();
462 }
463 return ImmutableList.copyOf(comments);
464 }
465
466 @CheckForNull
467 public Date selectedAt() {
468 return selectedAt;
469 }
470
471 public DefaultIssue setSelectedAt(@Nullable Date d) {
472 this.selectedAt = d;
473 return this;
474 }
475
476 @Override
477 public boolean equals(Object o) {
478 if (this == o) {
479 return true;
480 }
481 if (o == null || getClass() != o.getClass()) {
482 return false;
483 }
484 DefaultIssue that = (DefaultIssue) o;
485 if (key != null ? !key.equals(that.key) : that.key != null) {
486 return false;
487 }
488 return true;
489 }
490
491 @Override
492 public int hashCode() {
493 return key != null ? key.hashCode() : 0;
494 }
495
496 @Override
497 public String toString() {
498 return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
499 }
500
501 }