001/*-----------------------------------------------------------------------+
002 | com.teamscale.index
003 |                                                                       |
004   $Id$            
005 |                                                                       |
006 | Copyright (c)  2009-2012 CQSE GmbH                                 |
007 +-----------------------------------------------------------------------*/
008package org.conqat.engine.index.shared;
009
010import java.io.Serializable;
011import java.util.Comparator;
012
013import javax.annotation.Nullable;
014
015import org.conqat.engine.commons.findings.DetachedFinding;
016import org.conqat.engine.commons.findings.location.ElementLocation;
017import org.conqat.engine.commons.findings.location.TextRegionLocation;
018import org.conqat.lib.commons.js_export.ExportToJavaScript;
019
020import com.fasterxml.jackson.annotation.JsonCreator;
021import com.fasterxml.jackson.annotation.JsonIgnore;
022import com.fasterxml.jackson.annotation.JsonProperty;
023import com.thoughtworks.xstream.annotations.XStreamOmitField;
024
025/**
026 * A tracked finding extends the {@link DetachedFinding} by additional
027 * attributes that are calculated via tracking.
028 *
029 * <strong>This class is used for communication with IDE clients (via the
030 * {@link org.conqat.engine.service.shared.client.IdeServiceClient}), so special
031 * care has to be taken when changing its signature!</strong>
032 */
033@ExportToJavaScript
034public class TrackedFinding extends IndexFinding implements Serializable, Comparable<TrackedFinding> {
035
036        private static final String EQUALS_HASHCODE_NOT_SUPPORTED_MESSAGE = "Tracked findings should never be used in HashSets, "
037                        + "as equality should rely on the id, but the id must be mutable (which can cause havok when the object is part of a HashSet). "
038                        + "Please use a map with id as key instead!";
039
040        /** Version used for serialization. */
041        private static final long serialVersionUID = 1;
042
043        /** The name of the JSON property name for {@link #id}. */
044        private static final String ID_PROPERTY = "id";
045
046        /** The name of the JSON property name for {@link #birth}. */
047        private static final String BIRTH_PROPERTY = "birth";
048
049        /** The name of the JSON property name for {@link #death}. */
050        private static final String DEATH_PROPERTY = "death";
051
052        /**
053         * The partition in the finding index of this finding. We have to store it in
054         * the index (hence it is not transient, to include it in Java serialization),
055         * but we do not want our clients to see it (hence the JsonIgnore annotation).
056         */
057        @XStreamOmitField
058        @JsonIgnore
059        private final String findingIndexPartition;
060
061        /**
062         * The id of this finding. This is a hexadecimal representation of a MD5 hash
063         * code.
064         */
065        @JsonProperty(ID_PROPERTY)
066        private String id;
067
068        /** Commit of the birth of this finding. */
069        @JsonProperty(BIRTH_PROPERTY)
070        private final CommitDescriptor birth;
071
072        /**
073         * Commit of the death of this finding, i.e. the first commit where this did not
074         * exist anymore. While this is <code>null</code> the finding is still alive.
075         */
076        @JsonProperty(DEATH_PROPERTY)
077        @Nullable
078        private CommitDescriptor death = null;
079
080        public TrackedFinding(IndexFinding finding, String id, CommitDescriptor birthCommit, String findingIndexPartition) {
081                super(finding);
082                this.id = id;
083                this.birth = birthCommit;
084                this.findingIndexPartition = findingIndexPartition;
085        }
086
087        public TrackedFinding(IndexFinding finding, String id, CommitDescriptor birth, String findingIndexPartition,
088                        CommitDescriptor death) {
089                this(finding, id, birth, findingIndexPartition);
090                this.death = death;
091        }
092
093        /** Copy constructor. */
094        protected TrackedFinding(TrackedFinding other) {
095                super(other);
096                this.id = other.id;
097                this.birth = other.birth;
098                this.death = other.death;
099                this.findingIndexPartition = other.findingIndexPartition;
100        }
101
102        @JsonCreator
103        public TrackedFinding(@JsonProperty(GROUP_NAME_PROPERTY) String groupName,
104                        @JsonProperty(CATEGORY_NAME_PROPERTY) String categoryName, @JsonProperty(MESSAGE_PROPERTY) String message,
105                        @JsonProperty(LOCATION_PROPERTY) ElementLocation location, @JsonProperty(ID_PROPERTY) String id,
106                        @JsonProperty(BIRTH_PROPERTY) CommitDescriptor birthCommit,
107                        @JsonProperty(DEATH_PROPERTY) CommitDescriptor deathCommit) {
108                super(groupName, categoryName, message, location);
109                this.id = id;
110                this.birth = birthCommit;
111                this.death = deathCommit;
112                this.findingIndexPartition = null;
113        }
114
115        /** Returns the id of this finding. */
116        public String getId() {
117                return id;
118        }
119
120        public void setId(String id) {
121                this.id = id;
122        }
123
124        /** Returns the birth commit. */
125        public CommitDescriptor getBirthCommit() {
126                return birth;
127        }
128
129        /** Returns the partition in the findings index of this finding. */
130        public String getFindingIndexPartition() {
131                return findingIndexPartition;
132        }
133
134        /**
135         * Returns the death commit (or null for findings that are still alive).
136         */
137        public CommitDescriptor getDeathCommit() {
138                return death;
139        }
140
141        /**
142         * Sets the death commit. Use <code>null</code> to indicate that it is still
143         * alive.
144         */
145        public void setDeathCommit(CommitDescriptor death) {
146                this.death = death;
147        }
148
149        /** Returns whether this finding is still alive. */
150        public boolean isAlive() {
151                return death == null;
152        }
153
154        /**
155         * {@inheritDoc}
156         * <p>
157         * Sorts by ID.
158         */
159        @Override
160        public int compareTo(TrackedFinding other) {
161                return id.compareTo(other.id);
162        }
163
164        /**
165         * Returns the qualified group name, i.e. the concatenation of the category and
166         * group separated by /
167         */
168        public String getQualifiedGroupName() {
169                return getCategoryName() + "/" + getGroupName();
170        }
171
172        /** A basic comparator used for TrackedFindings. */
173        public static class TrackedFindingComparator implements Comparator<TrackedFinding> {
174
175                @Override
176                public int compare(TrackedFinding f1, TrackedFinding f2) {
177                        String p1 = f1.getLocation().getUniformPath();
178                        String p2 = f2.getLocation().getUniformPath();
179                        int pathComparison = p1.compareTo(p2);
180                        if (pathComparison != 0) {
181                                return pathComparison;
182                        }
183
184                        if (f1.getLocation() instanceof TextRegionLocation && f2.getLocation() instanceof TextRegionLocation) {
185                                TextRegionLocation l1 = (TextRegionLocation) f1.getLocation();
186                                TextRegionLocation l2 = (TextRegionLocation) f2.getLocation();
187                                int startDiff = l1.getRawStartLine() - l2.getRawStartLine();
188                                if (startDiff == 0) {
189                                        return l1.getRawEndLine() - l2.getRawEndLine();
190                                }
191                                return startDiff;
192                        }
193                        if (f1.getLocation() instanceof TextRegionLocation) {
194                                return -1;
195                        }
196                        if (f2.getLocation() instanceof TextRegionLocation) {
197                                return 1;
198                        }
199                        return 0;
200                }
201        }
202
203        @Override
204        public boolean equals(Object obj) {
205                throw new UnsupportedOperationException(EQUALS_HASHCODE_NOT_SUPPORTED_MESSAGE);
206        }
207
208        @Override
209        public int hashCode() {
210                throw new UnsupportedOperationException(EQUALS_HASHCODE_NOT_SUPPORTED_MESSAGE);
211        }
212}