001/*-------------------------------------------------------------------------+
002|                                                                          |
003| Copyright 2005-2011 the ConQAT Project                                   |
004|                                                                          |
005| Licensed under the Apache License, Version 2.0 (the "License");          |
006| you may not use this file except in compliance with the License.         |
007| You may obtain a copy of the License at                                  |
008|                                                                          |
009|    http://www.apache.org/licenses/LICENSE-2.0                            |
010|                                                                          |
011| Unless required by applicable law or agreed to in writing, software      |
012| distributed under the License is distributed on an "AS IS" BASIS,        |
013| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
014| See the License for the specific language governing permissions and      |
015| limitations under the License.                                           |
016+-------------------------------------------------------------------------*/
017package org.conqat.engine.commons.findings;
018
019import java.io.Serializable;
020import java.util.ArrayList;
021import java.util.Collection;
022import java.util.HashMap;
023import java.util.HashSet;
024import java.util.List;
025import java.util.Map;
026import java.util.Set;
027import java.util.function.Predicate;
028
029import javax.annotation.Nullable;
030
031import org.conqat.engine.commons.findings.location.ElementLocation;
032import org.conqat.lib.commons.assertion.CCSMAssert;
033import org.conqat.lib.commons.assessment.ETrafficLightColor;
034import org.conqat.lib.commons.collections.CollectionUtils;
035import org.conqat.lib.commons.collections.UnmodifiableMap;
036import org.conqat.lib.commons.js_export.ExportAsType;
037import org.conqat.lib.commons.js_export.ExportToJavaScript;
038
039import com.fasterxml.jackson.annotation.JsonCreator;
040import com.fasterxml.jackson.annotation.JsonProperty;
041
042/**
043 * This class describes orphaned finding that is attached to a node or a
044 * findings report. This is useful if e.g. findings have been filtered but
045 * certain operations should still be carried out on the findings.
046 */
047@ExportToJavaScript
048public class DetachedFinding implements Serializable {
049
050        /** Serial version UID. */
051        private static final long serialVersionUID = 1;
052
053        /** The name of the JSON property name for {@link #location}. */
054        protected static final String LOCATION_PROPERTY = "location";
055
056        /** The name of the JSON property name for {@link #groupName}. */
057        protected static final String GROUP_NAME_PROPERTY = "groupName";
058
059        /** The name of the JSON property name for {@link #categoryName}. */
060        protected static final String CATEGORY_NAME_PROPERTY = "categoryName";
061
062        /** The name of the JSON property name for {@link #message}. */
063        protected static final String MESSAGE_PROPERTY = "message";
064
065        /** The name of the JSON property name for {@link #assessment}. */
066        private static final String ASSESSMENT_PROPERTY = "assessment";
067
068        /** The location. */
069        @JsonProperty(LOCATION_PROPERTY)
070        @ExportAsType("!ts.data.ElementLocation|!ts.data.TextRegionLocation|!ts.data.QualifiedNameLocation")
071        private ElementLocation location;
072
073        /** The group name. */
074        @JsonProperty(GROUP_NAME_PROPERTY)
075        private String groupName;
076
077        /** The category name. */
078        @JsonProperty(CATEGORY_NAME_PROPERTY)
079        private String categoryName;
080
081        /** The message. */
082        @JsonProperty(MESSAGE_PROPERTY)
083        private String message;
084
085        /** The assessment color of the finding (may be null). */
086        @JsonProperty(ASSESSMENT_PROPERTY)
087        @Nullable
088        private ETrafficLightColor assessment;
089
090        /**
091         * The locations of other findings that are considered siblings. This is, e.g.,
092         * used to find other clone instances in the same clone class. As this is
093         * commonly empty, we keep this attribute null in this case to save space for
094         * serialization. The access methods, however, handle this transparently.
095         */
096        @JsonProperty("siblingLocations")
097        @Nullable
098        private List<ElementLocation> siblingLocations;
099
100        /**
101         * Caches the sibling locations as strings. Used to avoid adding duplicate
102         * sibling locations. Initialized lazily.
103         */
104        private transient Set<String> siblingLocationCache = null;
105
106        /**
107         * Next to the primary location {@link #location}, a finding may optionally have
108         * secondary locations. For instance, an architecture finding may contain the
109         * individual source code locations of the violating identifiers.
110         */
111        @JsonProperty("secondaryLocations")
112        @Nullable
113        private List<ElementLocation> secondaryLocations;
114
115        /**
116         * Properties for this finding. Each finding can be associated with one or more
117         * properties describing details of the finding (e.g. length of long method,
118         * etc.). These properties are also displayed in the UI and can be used for
119         * sorting findings (in which case the value class must be comparable).
120         */
121        @JsonProperty("properties")
122        @ExportAsType("!Object<string,!Object|string|number|boolean>")
123        private final Map<String, Object> properties = new HashMap<>();
124
125        /**
126         * @see #getStatementPath()
127         */
128        @JsonProperty("statementPath")
129        @Nullable
130        private List<StatementPathElement> statementPath = null;
131
132        /** Constructor. */
133        public DetachedFinding(String groupName, String categoryName, String message, ElementLocation location) {
134                this(groupName, categoryName, message, location, null);
135        }
136
137        /**
138         * Constructor. Adds the given finding properties to the finding. The assessment
139         * may be <code>null</code>.
140         */
141        public DetachedFinding(String groupName, String categoryName, String message, ElementLocation location,
142                        ETrafficLightColor assessment, Map<String, Object> findingProperties) {
143                this(groupName, categoryName, message, location, assessment);
144                properties.putAll(findingProperties);
145        }
146
147        /** Constructor. The assessment may be <code>null</code> */
148        @JsonCreator
149        public DetachedFinding(@JsonProperty(GROUP_NAME_PROPERTY) String groupName,
150                        @JsonProperty(CATEGORY_NAME_PROPERTY) String categoryName, @JsonProperty(MESSAGE_PROPERTY) String message,
151                        @JsonProperty(LOCATION_PROPERTY) ElementLocation location,
152                        @JsonProperty(ASSESSMENT_PROPERTY) ETrafficLightColor assessment) {
153                CCSMAssert.isNotNull(location);
154
155                this.groupName = groupName;
156                this.categoryName = categoryName;
157                this.message = message;
158                this.location = location;
159                this.assessment = assessment;
160        }
161
162        /** Copy constructor. */
163        protected DetachedFinding(DetachedFinding other) {
164                this(other.groupName, other.categoryName, other.message, other.location, other.assessment);
165
166                if (other.hasSiblings()) {
167                        this.siblingLocations = new ArrayList<>(other.siblingLocations);
168                }
169
170                if (other.secondaryLocations != null) {
171                        this.secondaryLocations = new ArrayList<>(other.secondaryLocations);
172                }
173
174                properties.putAll(other.properties);
175
176                this.statementPath = other.statementPath;
177        }
178
179        /** Get group name. */
180        public String getGroupName() {
181                return groupName;
182        }
183
184        /** Sets group name. */
185        public void setGroupName(String groupName) {
186                this.groupName = groupName;
187        }
188
189        /** Get category name. */
190        public String getCategoryName() {
191                return categoryName;
192        }
193
194        /** Sets category name. */
195        public void setCategoryName(String categoryName) {
196                this.categoryName = categoryName;
197        }
198
199        /** Get location. */
200        public ElementLocation getLocation() {
201                return location;
202        }
203
204        /** Sets location. */
205        public void setLocation(ElementLocation location) {
206                CCSMAssert.isNotNull(location);
207                this.location = location;
208        }
209
210        /** Get location string. */
211        public String getLocationString() {
212                return location.toLocationString();
213        }
214
215        /** Get message. */
216        public String getMessage() {
217                return message;
218        }
219
220        /** @see #message */
221        public void setMessage(String message) {
222                this.message = message;
223        }
224
225        /** Returns whether this findings has siblings. */
226        public boolean hasSiblings() {
227                return siblingLocations != null && !siblingLocations.isEmpty();
228        }
229
230        /** Returns the locations of sibling findings. */
231        public List<ElementLocation> getSiblingLocations() {
232                if (siblingLocations == null) {
233                        return CollectionUtils.emptyList();
234                }
235                return CollectionUtils.asUnmodifiable(siblingLocations);
236        }
237
238        /** Removes all sibling locations matching the given filter. */
239        public void removeMatchingSiblingLocations(Predicate<? super ElementLocation> filter) {
240                siblingLocations.removeIf(filter);
241        }
242
243        /**
244         * @see #secondaryLocations
245         */
246        public List<ElementLocation> getSecondaryLocations() {
247                if (secondaryLocations == null) {
248                        return CollectionUtils.emptyList();
249                }
250                return CollectionUtils.asUnmodifiable(secondaryLocations);
251        }
252
253        /**
254         * Adds all previously unknown locations of sibling findings (already known
255         * locations are skipped).
256         */
257        public void addSiblingLocation(ElementLocation location) {
258                if (siblingLocations == null) {
259                        siblingLocations = new ArrayList<>();
260                }
261                if (siblingLocationCache == null) {
262                        siblingLocationCache = new HashSet<>(
263                                        CollectionUtils.map(siblingLocations, ElementLocation::toLocationString));
264                        // This way, a finding can never be a sibling of itself
265                        siblingLocationCache.add(getLocationString());
266                }
267                String locationString = location.toLocationString();
268                if (!siblingLocationCache.contains(locationString)) {
269                        siblingLocations.add(location);
270                        siblingLocationCache.add(locationString);
271                }
272        }
273
274        /**
275         * Adds the given list of locations as sibling findings.
276         *
277         * @param findings
278         *            a collection of {@link DetachedFinding} that will be added as
279         *            siblings.
280         */
281        public void addSiblingFindings(Collection<DetachedFinding> findings) {
282                findings.forEach(this::addSiblingFinding);
283        }
284
285        /** Add the given sibling locations */
286        public void addSiblingLocations(Collection<ElementLocation> siblingLocations) {
287                siblingLocations.forEach(this::addSiblingLocation);
288        }
289
290        /** Adds the given {@link DetachedFinding} to the list of sibling findings. */
291        public void addSiblingFinding(DetachedFinding sibling) {
292                addSiblingLocation(sibling.getLocation());
293        }
294
295        /** Adds a secondary location to this finding. */
296        public void addSecondaryLocation(ElementLocation location) {
297                if (secondaryLocations == null) {
298                        secondaryLocations = new ArrayList<>();
299                }
300                secondaryLocations.add(location);
301        }
302
303        /** Adds the given locations as secondary locations. */
304        public void addSecondaryLocations(Collection<ElementLocation> locations) {
305                if (secondaryLocations == null) {
306                        secondaryLocations = new ArrayList<>();
307                }
308                secondaryLocations.addAll(locations);
309        }
310
311        /** Returns the properties of this finding. */
312        public UnmodifiableMap<String, Object> getProperties() {
313                return CollectionUtils.asUnmodifiable(properties);
314        }
315
316        /** Sets a property for this finding. */
317        public void setProperty(String name, Object value) {
318                properties.put(name, value);
319        }
320
321        /** Returns assessment. */
322        public ETrafficLightColor getAssessment() {
323                return assessment;
324        }
325
326        /** Sets the assessment. */
327        public void setAssessment(ETrafficLightColor assessment) {
328                this.assessment = assessment;
329        }
330
331        /**
332         * Returns a string representation that contains the, message, the group and and
333         * location hint.
334         */
335        @Override
336        public String toString() {
337                return getMessage() + " (" + getGroupName() + ") @ " + location.toLocationString();
338        }
339
340        /**
341         * Returns the statement path if there is one attached to this finding.
342         *
343         * A statementPath is a path through the program that leads to this finding. The
344         * path may not be a simple chain, but could be a DAG.
345         *
346         * Each entry in the list refers to the indices of its predecessor statements.
347         * Contract for the setup of this structure:
348         * <ul>
349         * <li>An element might have more than one predecessor.</li>
350         * <li>Multiple elements can have the same predecessor.</li>
351         * <li>There are no cycles in the path.</li>
352         * </ul>
353         */
354        public List<StatementPathElement> getStatementPath() {
355                if (statementPath == null) {
356                        return CollectionUtils.emptyList();
357                }
358                return statementPath;
359        }
360
361        /** @see #getStatementPath() */
362        public void setStatementPath(List<StatementPathElement> path) {
363                this.statementPath = path;
364        }
365
366}