xref: /OpenGrok/opengrok-indexer/src/main/java/org/opengrok/indexer/history/Annotation.java (revision 5fa6eb1956a3d749703cfc071f642a1567868860)
1 /*
2  * CDDL HEADER START
3  *
4  * The contents of this file are subject to the terms of the
5  * Common Development and Distribution License (the "License").
6  * You may not use this file except in compliance with the License.
7  *
8  * See LICENSE.txt included in this distribution for the specific
9  * language governing permissions and limitations under the License.
10  *
11  * When distributing Covered Code, include this CDDL HEADER in each
12  * file and include the License file at LICENSE.txt.
13  * If applicable, add the following below this CDDL HEADER, with the
14  * fields enclosed by brackets "[]" replaced with your own identifying
15  * information: Portions Copyright [yyyy] [name of copyright owner]
16  *
17  * CDDL HEADER END
18  */
19 
20 /*
21  * Copyright (c) 2007, 2022, Oracle and/or its affiliates. All rights reserved.
22  * Portions Copyright (c) 2019, Krystof Tulinger <k.tulinger@seznam.cz>.
23  * Portions Copyright (c) 2020, Chris Fraire <cfraire@me.com>.
24  */
25 package org.opengrok.indexer.history;
26 
27 import org.jetbrains.annotations.TestOnly;
28 import org.opengrok.indexer.logger.LoggerFactory;
29 import org.opengrok.indexer.util.Color;
30 import org.opengrok.indexer.util.LazilyInstantiate;
31 import org.opengrok.indexer.util.RainbowColorGenerator;
32 
33 import java.io.IOException;
34 import java.io.StringWriter;
35 import java.io.Writer;
36 import java.util.ArrayList;
37 import java.util.Comparator;
38 import java.util.HashMap;
39 import java.util.HashSet;
40 import java.util.List;
41 import java.util.Map;
42 import java.util.Map.Entry;
43 import java.util.Set;
44 import java.util.logging.Logger;
45 import java.util.stream.Collectors;
46 
47 /**
48  * Class representing file annotation, i.e., revision and author for the last
49  * modification of each line in the file.
50  */
51 public class Annotation {
52 
53     private static final Logger LOGGER = LoggerFactory.getLogger(Annotation.class);
54 
55     private final List<Line> lines = new ArrayList<>();
56     private final Map<String, String> desc = new HashMap<>(); // revision to description
57     private final Map<String, Integer> fileVersions = new HashMap<>(); // maps revision to file version
58     private final LazilyInstantiate<Map<String, String>> colors = LazilyInstantiate.using(this::generateColors);
59     private int widestRevision;
60     private int widestAuthor;
61     private final String filename;
62 
Annotation(String filename)63     public Annotation(String filename) {
64         this.filename = filename;
65     }
66 
67     /**
68      * Gets the revision for the last change to the specified line.
69      *
70      * @param line line number (counting from 1)
71      * @return revision string, or an empty string if there is no information
72      * about the specified line
73      */
getRevision(int line)74     public String getRevision(int line) {
75         try {
76             return lines.get(line - 1).revision;
77         } catch (IndexOutOfBoundsException e) {
78             return "";
79         }
80     }
81 
82     /**
83      * Gets all revisions that are in use, first is the lowest one (sorted using natural order).
84      *
85      * @return list of all revisions the file has
86      */
getRevisions()87     public Set<String> getRevisions() {
88         Set<String> ret = new HashSet<>();
89         for (Line ln : this.lines) {
90             ret.add(ln.revision);
91         }
92         return ret;
93     }
94 
95     @TestOnly
getAuthors()96     Set<String> getAuthors() {
97         return lines.stream().map(ln -> ln.author).collect(Collectors.toSet());
98     }
99 
100     /**
101      * Gets the author who last modified the specified line.
102      *
103      * @param line line number (counting from 1)
104      * @return author, or an empty string if there is no information about the
105      * specified line
106      */
getAuthor(int line)107     public String getAuthor(int line) {
108         try {
109             return lines.get(line - 1).author;
110         } catch (IndexOutOfBoundsException e) {
111             return "";
112         }
113     }
114 
115     /**
116      * Gets the enabled state for the last change to the specified line.
117      *
118      * @param line line number (counting from 1)
119      * @return true if the xref for this revision is enabled, false otherwise
120      */
isEnabled(int line)121     public boolean isEnabled(int line) {
122         try {
123             return lines.get(line - 1).enabled;
124         } catch (IndexOutOfBoundsException e) {
125             return false;
126         }
127     }
128 
129     /**
130      * Returns the size of the file (number of lines).
131      *
132      * @return number of lines
133      */
size()134     public int size() {
135         return lines.size();
136     }
137 
138     /**
139      * Returns the widest revision string in the file (used for pretty
140      * printing).
141      *
142      * @return number of characters in the widest revision string
143      */
getWidestRevision()144     public int getWidestRevision() {
145         return widestRevision;
146     }
147 
148     /**
149      * Returns the widest author name in the file (used for pretty printing).
150      *
151      * @return number of characters in the widest author string
152      */
getWidestAuthor()153     public int getWidestAuthor() {
154         return widestAuthor;
155     }
156 
157     /**
158      * Adds a line to the file.
159      *
160      * @param revision revision number
161      * @param author author name
162      */
addLine(String revision, String author, boolean enabled)163     void addLine(String revision, String author, boolean enabled) {
164         final Line line = new Line(revision, author, enabled);
165         lines.add(line);
166         widestRevision = Math.max(widestRevision, line.revision.length());
167         widestAuthor = Math.max(widestAuthor, line.author.length());
168     }
169 
addDesc(String revision, String description)170     void addDesc(String revision, String description) {
171         desc.put(revision, description);
172     }
173 
getDesc(String revision)174     public String getDesc(String revision) {
175         return desc.get(revision);
176     }
177 
addFileVersion(String revision, int fileVersion)178     void addFileVersion(String revision, int fileVersion) {
179         fileVersions.put(revision, fileVersion);
180     }
181 
182     /**
183      * Translates repository revision number into file version.
184      * @param revision revision number
185      * @return file version number. 0 if unknown. 1 first version of file, etc.
186      */
getFileVersion(String revision)187     public int getFileVersion(String revision) {
188         return fileVersions.getOrDefault(revision, 0);
189     }
190 
191     /**
192      * @return Count of revisions on this file.
193      */
getFileVersionsCount()194     public int getFileVersionsCount() {
195         return fileVersions.size();
196     }
197 
198     /**
199      * Return the color palette for the annotated file.
200      *
201      * @return map of (revision, css color string) for each revision in {@code getRevisions()}
202      * @see #generateColors()
203      */
getColors()204     public Map<String, String> getColors() {
205         return colors.get();
206     }
207 
208     /**
209      * Generate the color palette for the annotated revisions.
210      * <p>
211      * First, take into account revisions which are tracked in history fields
212      * and compute their color. Secondly, use all other revisions in order
213      * which is undefined and generate the rest of the colors for them.
214      *
215      * @return map of (revision, css color string) for each revision in {@code getRevisions()}
216      * @see #getRevisions()
217      */
generateColors()218     private Map<String, String> generateColors() {
219         List<Color> colors = RainbowColorGenerator.getOrderedColors();
220 
221         Map<String, String> colorMap = new HashMap<>();
222         final List<String> revisions =
223                 getRevisions()
224                         .stream()
225                         /*
226                          * Greater file version means more recent revision.
227                          * 0 file version means unknown revision (untracked by history entries).
228                          *
229                          * The result of this sort is:
230                          * 1) known revisions sorted from most recent to least recent
231                          * 2) all other revisions in non-determined order
232                          */
233                         .sorted(Comparator.comparingInt(this::getFileVersion).reversed())
234                         .collect(Collectors.toList());
235 
236         final int nColors = colors.size();
237         final double colorsPerBucket = (double) nColors / getRevisions().size();
238 
239         revisions.forEach(revision -> {
240             final int lineVersion = getRevisions().size() - getFileVersion(revision);
241             final double bucketTotal = colorsPerBucket * lineVersion;
242             final int bucketIndex = (int) Math.max(
243                     Math.min(Math.floor(bucketTotal), nColors - 1.0), 0);
244             Color color = colors.get(bucketIndex);
245             colorMap.put(revision, String.format("rgb(%d, %d, %d)", color.red, color.green, color.blue));
246         });
247 
248         return colorMap;
249     }
250 
251     /** Class representing one line in the file. */
252     private static class Line {
253         final String revision;
254         final String author;
255         final boolean enabled;
Line(String rev, String aut, boolean ena)256         Line(String rev, String aut, boolean ena) {
257             revision = (rev == null) ? "" : rev;
258             author = (aut == null) ? "" : aut;
259             enabled = ena;
260         }
261     }
262 
getFilename()263     public String getFilename() {
264         return filename;
265     }
266 
267     //TODO below might be useless, need to test with more SCMs and different commit messages
268     // to see if it will not be useful, if title attribute of <a> loses it's breath
writeTooltipMap(Writer out)269     public void writeTooltipMap(Writer out) throws IOException {
270         out.append("<script type=\"text/javascript\">\nvar desc = new Object();\n");
271         for (Entry<String, String> entry : desc.entrySet()) {
272             out.append("desc['");
273             out.append(entry.getKey());
274             out.append("'] = \"");
275             out.append(entry.getValue());
276             out.append("\";\n");
277         }
278         out.append("</script>\n");
279     }
280 
281     @Override
toString()282     public String toString() {
283         StringWriter sw = new StringWriter();
284         for (Line line : lines) {
285             sw.append(line.revision);
286             sw.append("|");
287             sw.append(line.author);
288             sw.append(": \n");
289         }
290 
291         try {
292             writeTooltipMap(sw);
293         } catch (IOException e) {
294             LOGGER.finest(e.getMessage());
295         }
296 
297         return sw.toString();
298     }
299 }
300