/*
* 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<Integer> lines = new ArrayList<Integer>();
private ArrayList<RevInfo> infos = new ArrayList<RevInfo>();
private TreeMap<String,Integer> rev2rid = new TreeMap<String,Integer>();
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<String> 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 = "&lt;br/&gt;";
static final String BR = "<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 <a title=""> 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("&#92;");
break;
case '"':
sb.append("&quot;");
break; // \\\"
case '&':
sb.append("&amp;");
break;
case '>':
sb.append("&gt;");
break;
case '<':
sb.append("&lt;");
break;
case ' ':
sb.append("&nbsp;");
break;
case '\t':
sb.append("&nbsp;&nbsp;&nbsp;&nbsp;");
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
"<b>Changeset</b>: " + encode(changeset, BR) + BR
+ "<b>Summary</b>: " + encode(msg, BR) + BR
+ "<b>User</b>: " + encode(user, BR) + BR
+ "<b>Date</b>: " + 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 '&gt;br/&lt;).
*/
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();
}
}