/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License (the "License"). * You may not use this file except in compliance with the License. * * See LICENSE.txt included in this distribution for the specific * language governing permissions and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at LICENSE.txt. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END */ /* * Copyright (c) 2007, 2012, Oracle and/or its affiliates. All rights reserved. * Portions Copyright 2012 Jens Elkner. */ package org.opensolaris.opengrok.history; import java.util.ArrayList; import java.util.Date; import java.util.Set; import java.util.TreeMap; import java.util.logging.Logger; import org.opensolaris.opengrok.web.Util; /** * Class representing file annotation, i.e., revision and author for the last * modification of each line in the file. */ public class Annotation { private final ArrayList lines = new ArrayList(); private ArrayList infos = new ArrayList(); private TreeMap rev2rid = new TreeMap(); private int widestRevision; private int widestAuthor; private final String filename; static final Logger log = Logger.getLogger(Annotation.class.getName()); /** * Create a new instance. * @param filename the basename of the associated file. */ public Annotation(String filename) { this.filename = filename; } private final int getRID(int line) { if (line > lines.size()) { return -1; } return lines.get(line-1).intValue(); } /** * Gets the revision for the last change to the specified line. * * @param line line number (counting from 1) * @return revision string, or an empty string if there is no information * about the specified line */ public String getRevision(int line) { int idx = getRID(line); return idx < 0 ? "" : infos.get(idx).rev; } /** * Gets all revisions that are in use, first is the lowest one (sorted using * natural order) * * @return list of all revisions the file has */ public Set getRevisions() { return rev2rid.keySet(); } /** * Gets the author who last modified the specified line. * * @param line line number (counting from 1) * @return author, or an empty string if there is no information about the * specified line */ public String getAuthor(int line) { int idx = getRID(line); return idx < 0 ? "" : infos.get(idx).author; } /** * Returns the size of the file (number of lines). * * @return number of lines */ public int size() { return lines.size(); } /** * Returns the widest revision string in the file (used for pretty * printing). * * @return number of characters in the widest revision string */ public int getWidestRevision() { return widestRevision; } /** * Returns the widest author name in the file (used for pretty printing). * * @return number of characters in the widest author string */ public int getWidestAuthor() { return widestAuthor; } private class RevInfo { String rev; // revision column in html view String author; // email aka author column in html view Desc desc; // tooltip infos public RevInfo(String rev, String author, Desc desc) { this.rev = rev; this.author = author; this.desc = desc; } } /** * Adds a line to the file. Invokeing concurrently causes unpredictable * results. * * @param revision revision number * @param author author name */ void addLine(String revision, String author) { if (revision == null) { revision = ""; } if (author == null) { author = ""; } Integer rid = rev2rid.get(revision); if (rid == null) { RevInfo info = new RevInfo(revision, author, null); rid = Integer.valueOf(infos.size()); rev2rid.put(revision, rid); infos.add(info); widestRevision = Math.max(widestRevision, revision.length()); widestAuthor = Math.max(widestAuthor, info.author.length()); } lines.add(rid); } private class Desc { String changeset; String msg; String html; String user; Date date; static final String EOL = "<br/>"; static final String BR = "
"; /** * Converts different html special characters into their encodings used in * html. Currently used only for tooltips of annotation revision number view * * @param s * input text * @return encoded text for use in tag */ private final String encode(String s, String newline) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < s.length(); i++ ) { char c = s.charAt(i); switch (c) { case '\\': sb.append("\"); break; case '"': sb.append("""); break; // \\\" case '&': sb.append("&"); break; case '>': sb.append(">"); break; case '<': sb.append("<"); break; case ' ': sb.append(" "); break; case '\t': sb.append("    "); break; case '\n': sb.append(newline); break; case '\r': break; default: sb.append(c); break; } } return sb.toString(); } Desc(String changeset, String msg, String user, Date date) { this.changeset = changeset; this.msg = msg; this.user = user; this.date = date; } String getHtml() { if (html == null) { html = "changeset: " + encode(changeset, EOL) + EOL + "summary: " + encode(msg, EOL) + EOL + "user: " + encode(user, EOL) + EOL + "date: " + date; } return html; } String getJson() { // not stored because usually called once only return "Changeset: " + encode(changeset, BR) + BR + "Summary: " + encode(msg, BR) + BR + "User: " + encode(user, BR) + BR + "Date: " + date; } } /** * Add the full commit message to the given revision. Ignored if there is * no revision, which may accommodate this message (add an appropriate line * before, if desired). * @param revision revision in question. * @param description full commit message to add (gets automatically html * escaped including newlines, which are translated into '>br/<). */ void addDesc(String revision, String changeset, String msg, String user, Date date) { Integer rid = rev2rid.get(revision); if (rid != null) { RevInfo info = infos.get(rid.intValue()); if (info != null) { info.desc = new Desc(changeset, msg, user, date); } } } /** * Get the full commit message for the given revision. * @param revision revision in question. * @return {@code null} if not found, the html escaped commit message * otherwise. */ public String getDesc(String revision) { Integer idx = rev2rid.get(revision); return idx == null ? null : infos.get(idx.intValue()).desc.getHtml(); } /** * Get the basename of the associated source file. * @return a filename * @see Annotation#Annotation(String) */ public String getFilename() { return filename; } /** * Convert this annotation data into compact JSON format. * @return a JSON Object. */ public String toJson() { StringBuilder buf = new StringBuilder(256) .append("{\"uri\":\"").append(Util.uriEncodePath(filename)) .append("\",\"lines\":").append(lines.size()) .append(",\"line2rev\":["); if (lines.size() > 0) { Integer lastRid = Integer.valueOf(-1); int count = 0; for (Integer rid : lines) { count++; if (rid.intValue() == lastRid.intValue()) { continue; } buf.append(count).append(',').append(rid).append(','); lastRid = rid; } buf.setLength(buf.length()-1); // delete last , } buf.append("], \"revs\": ["); if (infos.size() > 0) { for (RevInfo info : infos) { buf.append("{\"rev\":").append(Util.jsStringLiteral(info.rev)) .append(",\"author\":").append(Util.jsStringLiteral(info.author)) .append(",\"msg\":\"").append(info.desc.getJson()) .append("\"},"); } buf.setLength(buf.length()-1); } return buf.append("]}").toString(); } }