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