/* * SonarQube * Copyright (C) 2009-2017 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonar.core.issue; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; import java.io.Serializable; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.builder.ToStringBuilder; import org.apache.commons.lang.builder.ToStringStyle; import org.sonar.api.issue.Issue; import org.sonar.api.issue.IssueComment; import org.sonar.api.rule.RuleKey; import org.sonar.api.rule.Severity; import org.sonar.api.rules.RuleType; import org.sonar.api.utils.Duration; import org.sonar.core.issue.tracking.Trackable; import static java.lang.String.format; public class DefaultIssue implements Issue, Trackable, org.sonar.api.ce.measure.Issue { private String key; private RuleType type; private String componentUuid; private String componentKey; private String moduleUuid; private String moduleUuidPath; private String projectUuid; private String projectKey; private RuleKey ruleKey; private String language; private String severity; private boolean manualSeverity = false; private String message; private Integer line; private Double gap; private Duration effort; private String status; private String resolution; private String assignee; private String checksum; private Map<String, String> attributes = null; private String authorLogin = null; private List<IssueComment> comments = null; private Set<String> tags = null; // temporarily an Object as long as DefaultIssue is used by sonar-batch private Object locations = null; // FUNCTIONAL DATES private Date creationDate; private Date updateDate; private Date closeDate; // FOLLOWING FIELDS ARE AVAILABLE ONLY DURING SCAN // Current changes private FieldDiffs currentChange = null; // all changes private List<FieldDiffs> changes = null; // true if the the issue did not exist in the previous scan. private boolean isNew = true; // True if the the issue did exist in the previous scan but not in the current one. That means // that this issue should be closed. private boolean beingClosed = false; private boolean onDisabledRule = false; // true if some fields have been changed since the previous scan private boolean isChanged = false; // true if notifications have to be sent private boolean sendNotifications = false; // Date when issue was loaded from db (only when isNew=false) private Long selectedAt; @Override public String key() { return key; } public DefaultIssue setKey(String key) { this.key = key; return this; } @Override public RuleType type() { return type; } public DefaultIssue setType(RuleType type) { this.type = type; return this; } /** * Can be null on Views or Devs */ @Override @CheckForNull public String componentUuid() { return componentUuid; } public DefaultIssue setComponentUuid(@Nullable String s) { this.componentUuid = s; return this; } @Override public String componentKey() { return componentKey; } public DefaultIssue setComponentKey(String s) { this.componentKey = s; return this; } @CheckForNull public String moduleUuid() { return moduleUuid; } public DefaultIssue setModuleUuid(@Nullable String moduleUuid) { this.moduleUuid = moduleUuid; return this; } @CheckForNull public String moduleUuidPath() { return moduleUuidPath; } public DefaultIssue setModuleUuidPath(@Nullable String moduleUuidPath) { this.moduleUuidPath = moduleUuidPath; return this; } /** * Can be null on Views or Devs */ @Override @CheckForNull public String projectUuid() { return projectUuid; } public DefaultIssue setProjectUuid(@Nullable String projectUuid) { this.projectUuid = projectUuid; return this; } @Override public String projectKey() { return projectKey; } public DefaultIssue setProjectKey(String projectKey) { this.projectKey = projectKey; return this; } @Override public RuleKey ruleKey() { return ruleKey; } public DefaultIssue setRuleKey(RuleKey k) { this.ruleKey = k; return this; } @Override public String language() { return language; } public DefaultIssue setLanguage(String l) { this.language = l; return this; } @Override public String severity() { return severity; } public DefaultIssue setSeverity(@Nullable String s) { Preconditions.checkArgument(s == null || Severity.ALL.contains(s), "Not a valid severity: " + s); this.severity = s; return this; } public boolean manualSeverity() { return manualSeverity; } public DefaultIssue setManualSeverity(boolean b) { this.manualSeverity = b; return this; } @Override @CheckForNull public String message() { return message; } public DefaultIssue setMessage(@Nullable String s) { this.message = StringUtils.abbreviate(StringUtils.trim(s), MESSAGE_MAX_SIZE); return this; } @Override @CheckForNull public Integer line() { return line; } public DefaultIssue setLine(@Nullable Integer l) { Preconditions.checkArgument(l == null || l > 0, format("Line must be null or greater than zero (got %d)", l)); this.line = l; return this; } /** * @deprecated since5.5, replaced by {@link #gap()} */ @Deprecated @Override @CheckForNull public Double effortToFix() { return gap(); } @Override @CheckForNull public Double gap() { return gap; } public DefaultIssue setGap(@Nullable Double d) { Preconditions.checkArgument(d == null || d >= 0, format("Gap must be greater than or equal 0 (got %s)", d)); this.gap = d; return this; } /** * @deprecated since5.5, replaced by {@link #effort()} */ @Deprecated @Override @CheckForNull public Duration debt() { return effort(); } /** * Elapsed time to fix the issue */ @Override @CheckForNull public Duration effort() { return effort; } @CheckForNull public Long effortInMinutes() { return effort != null ? effort.toMinutes() : null; } public DefaultIssue setEffort(@Nullable Duration t) { this.effort = t; return this; } @Override public String status() { return status; } public DefaultIssue setStatus(String s) { Preconditions.checkArgument(!Strings.isNullOrEmpty(s), "Status must be set"); this.status = s; return this; } @Override @CheckForNull public String resolution() { return resolution; } public DefaultIssue setResolution(@Nullable String s) { this.resolution = s; return this; } /** * @deprecated since 5.5, manual issue feature has been dropped. */ @Deprecated @Override @CheckForNull public String reporter() { return null; } @Override @CheckForNull public String assignee() { return assignee; } public DefaultIssue setAssignee(@Nullable String s) { this.assignee = s; return this; } @Override public Date creationDate() { return creationDate; } public DefaultIssue setCreationDate(Date d) { this.creationDate = truncateToSeconds(d); return this; } @CheckForNull private static Date truncateToSeconds(@Nullable Date d) { if (d == null) { return null; } Instant instant = d.toInstant(); instant = instant.truncatedTo(ChronoUnit.SECONDS); return Date.from(instant); } @Override @CheckForNull public Date updateDate() { return updateDate; } public DefaultIssue setUpdateDate(@Nullable Date d) { this.updateDate = truncateToSeconds(d); return this; } @Override @CheckForNull public Date closeDate() { return closeDate; } public DefaultIssue setCloseDate(@Nullable Date d) { this.closeDate = truncateToSeconds(d); return this; } @CheckForNull public String checksum() { return checksum; } public DefaultIssue setChecksum(@Nullable String s) { this.checksum = s; return this; } @Override public boolean isNew() { return isNew; } public DefaultIssue setNew(boolean b) { isNew = b; return this; } /** * True when one of the following conditions is true : * <ul> * <li>the related component has been deleted or renamed</li> * <li>the rule has been deleted (eg. on plugin uninstall)</li> * <li>the rule has been disabled in the Quality profile</li> * </ul> */ public boolean isBeingClosed() { return beingClosed; } public DefaultIssue setBeingClosed(boolean b) { beingClosed = b; return this; } public boolean isOnDisabledRule() { return onDisabledRule; } public DefaultIssue setOnDisabledRule(boolean b) { onDisabledRule = b; return this; } public boolean isChanged() { return isChanged; } public DefaultIssue setChanged(boolean b) { isChanged = b; return this; } public boolean mustSendNotifications() { return sendNotifications; } public DefaultIssue setSendNotifications(boolean b) { sendNotifications = b; return this; } @Override @CheckForNull public String attribute(String key) { return attributes == null ? null : attributes.get(key); } public DefaultIssue setAttribute(String key, @Nullable String value) { if (attributes == null) { attributes = Maps.newHashMap(); } if (value == null) { attributes.remove(key); } else { attributes.put(key, value); } return this; } @Override public Map<String, String> attributes() { return attributes == null ? Collections.<String, String>emptyMap() : ImmutableMap.copyOf(attributes); } public DefaultIssue setAttributes(@Nullable Map<String, String> map) { if (map != null) { if (attributes == null) { attributes = Maps.newHashMap(); } attributes.putAll(map); } return this; } @Override @CheckForNull public String authorLogin() { return authorLogin; } public DefaultIssue setAuthorLogin(@Nullable String s) { this.authorLogin = s; return this; } @Override @CheckForNull public String actionPlanKey() { // In 5.5, action plan is dropped. return null; } public DefaultIssue setFieldChange(IssueChangeContext context, String field, @Nullable Serializable oldValue, @Nullable Serializable newValue) { if (!Objects.equals(oldValue, newValue)) { if (currentChange == null) { currentChange = new FieldDiffs(); currentChange.setUserLogin(context.login()); currentChange.setCreationDate(context.date()); } currentChange.setDiff(field, oldValue, newValue); } addChange(currentChange); return this; } public DefaultIssue setCurrentChange(FieldDiffs currentChange) { this.currentChange = currentChange; addChange(currentChange); return this; } @CheckForNull public FieldDiffs currentChange() { return currentChange; } public DefaultIssue addChange(FieldDiffs change) { if (changes == null) { changes = new ArrayList<>(); } changes.add(change); return this; } public DefaultIssue setChanges(List<FieldDiffs> changes) { this.changes = changes; return this; } public List<FieldDiffs> changes() { if (changes == null) { return Collections.emptyList(); } return ImmutableList.copyOf(changes); } public DefaultIssue addComment(DefaultIssueComment comment) { if (comments == null) { comments = new ArrayList<>(); } comments.add(comment); return this; } @Override public List<IssueComment> comments() { if (comments == null) { return Collections.emptyList(); } return ImmutableList.copyOf(comments); } @CheckForNull public Long selectedAt() { return selectedAt; } public DefaultIssue setSelectedAt(@Nullable Long d) { this.selectedAt = d; return this; } @CheckForNull public <T> T getLocations() { return (T) locations; } public DefaultIssue setLocations(@Nullable Object locations) { this.locations = locations; return this; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } DefaultIssue that = (DefaultIssue) o; return !(key != null ? !key.equals(that.key) : (that.key != null)); } @Override public int hashCode() { return key != null ? key.hashCode() : 0; } @Override public String toString() { return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); } @Override public Set<String> tags() { if (tags == null) { return ImmutableSet.of(); } else { return ImmutableSet.copyOf(tags); } } public DefaultIssue setTags(Collection<String> tags) { this.tags = new LinkedHashSet<>(tags); return this; } @Override public Integer getLine() { return line; } @Override public String getMessage() { return message; } @Override public String getLineHash() { return checksum; } @Override public RuleKey getRuleKey() { return ruleKey; } }