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