/*
* 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) 2005, 2012, Oracle and/or its affiliates. All rights reserved.
* Portions Copyright 2011, 2012 Jens Elkner.
*/
package org.opensolaris.opengrok.web;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.Writer;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;
import org.opensolaris.opengrok.Info;
import org.opensolaris.opengrok.configuration.Configuration;
import org.opensolaris.opengrok.configuration.RuntimeEnvironment;
import org.opensolaris.opengrok.history.HistoryException;
import org.opensolaris.opengrok.history.HistoryGuru;
import org.opensolaris.opengrok.util.IOUtils;
/**
* Class for useful functions.
*/
public final class Util {
private static final Logger logger = Logger.getLogger(Util.class.getName());
private Util() {
// singleton
}
/**
* Replace all HTML special characters '&', '<', '>' in the given String
* with their corresponding HTML entity references.
* @param s string to htmlize
* @return <var>s</var> if it doesn't contain a special character, a new
* string otherwise.
*/
public static String htmlize(String s) {
int start = -1;
for (int i=0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '>' || c == '<' || c == '&') {
start = i;
break;
}
}
if (start == -1) {
return s;
}
StringBuilder sb = new StringBuilder(s.substring(0, start));
sb.ensureCapacity(s.length() + 8);
// since we can't copy sequences efficiently we copy char by char :(
for (int i=start; i < s.length(); i++) {
char c = s.charAt(i);
switch (c) {
case '&':
sb.append("&amp;");
break;
case '>':
sb.append("&gt;");
break;
case '<':
sb.append("&lt;");
break;
default:
sb.append(c);
}
}
return sb.toString();
}
/**
* Replace all HTML special characters '&', '<', '>' AND linefeeds ('\n')
* in the given String with their corresponding HTML entity references.
* @param s string to htmlize
* @param eol the replacement to use for linefeeds ('\n')
* @return <var>s</var> if it doesn't contain a special character, a new
* string otherwise.
*/
public static String htmlize(String s, String eol) {
if (eol == null) {
eol = "\\n";
}
int start = -1;
for (int i=0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '>' || c == '<' || c == '&' || c == '\n') {
start = i;
break;
}
}
if (start == -1) {
return s;
}
StringBuilder sb = new StringBuilder(s.substring(0, start));
sb.ensureCapacity(s.length() + 8);
// since we can't copy sequences efficiently we copy char by char :(
for (int i=start; i < s.length(); i++) {
char c = s.charAt(i);
switch (c) {
case '&':
sb.append("&amp;");
break;
case '>':
sb.append("&gt;");
break;
case '<':
sb.append("&lt;");
break;
case '\n':
sb.append(eol);
break;
default:
sb.append(c);
}
}
return sb.toString();
}
private static String versionP = htmlize(Info.getRevision());
/**
* used by BUI - CSS needs this parameter for proper cache refresh (per
* changeset) in client browser jel: but useless, since the page cached
* anyway.
*
* @return html escaped version (hg changeset)
*/
public static String versionParameter() {
return versionP;
}
/**
* Convinience method for {@code breadcrumbPath(urlPrefix, path, '/')}.
* @param urlPrefix prefix to add to each url
* @param path path to crack
* @return HTML markup fro the breadcrumb or the path itself.
*
* @see #breadcrumbPath(String, String, char)
*/
public static String breadcrumbPath(String urlPrefix, String path) {
return breadcrumbPath(urlPrefix, path, '/');
}
private static final String anchorLinkStart = "<a href=\"";
private static final String anchorEnd = "</a>";
private static final String closeQuotedTag = "\">";
/**
* Convenience method for
* {@code breadcrumbPath(urlPrefix, path, sep, "", false)}.
*
* @param urlPrefix prefix to add to each url
* @param path path to crack
* @param sep separator to use to crack the given path
*
* @return HTML markup fro the breadcrumb or the path itself.
* @see #breadcrumbPath(String, String, char, String, boolean, boolean)
*/
public static String breadcrumbPath(String urlPrefix, String path, char sep)
{
return breadcrumbPath(urlPrefix, path, sep, "", false);
}
/**
* Convenience method for
* {@code breadcrumbPath(urlPrefix, path, sep, "", false, path.endsWith(sep)}.
*
* @param urlPrefix prefix to add to each url
* @param path path to crack
* @param sep separator to use to crack the given path
* @param urlPostfix suffix to add to each url
* @param compact if {@code true} the given path gets transformed into
* its canonical form (.i.e. all '.' and '..' and double separators
* removed, but not always resolves to an absolute path) before processing
* starts.
* @return HTML markup fro the breadcrumb or the path itself.
* @see #breadcrumbPath(String, String, char, String, boolean, boolean)
* @see #getCanonicalPath(String, char)
*/
public static String breadcrumbPath(String urlPrefix, String path,
char sep, String urlPostfix, boolean compact)
{
if (path == null || path.length() == 0) {
return path;
}
return breadcrumbPath(urlPrefix, path, sep, urlPostfix, compact,
path.charAt(path.length() - 1) == sep);
}
/**
* Create a breadcrumb path to allow navigation to each element of a path.
* Consecutive separators (<var>sep</var>) in the given <var>path</var> are
* always collapsed into a single separator automatically. If
* <var>compact</var> is {@code true} path gets translated into a canonical
* path similar to {@link File#getCanonicalPath()}, however the current
* working directory is assumed to be "/" and no checks are done (e.g.
* neither whether the path [component] exists nor which type it is).
*
* @param urlPrefix
* what should be prepend to the constructed URL
* @param path
* the full path from which the breadcrumb path is built.
* @param sep
* the character that separates the path components in
* <var>path</var>
* @param urlPostfix
* what should be append to the constructed URL
* @param compact
* if {@code true}, a canonical path gets constructed before
* processing.
* @param isDir
* if {@code true} a "/" gets append to the last path component's
* link and <var>sep</var> to its name
* @return <var>path</var> if it resolves to an empty or "/" or
* {@code null} path, the HTML markup for the breadcrumb path otherwise.
*/
public static String breadcrumbPath(String urlPrefix, String path,
char sep, String urlPostfix, boolean compact, boolean isDir)
{
if (path == null || path.length() == 0) {
return path;
}
String[] pnames = normalize(path.split(escapeForRegex(sep)), compact);
if (pnames.length == 0) {
return path;
}
String prefix = urlPrefix == null ? "" : urlPrefix;
String postfix = urlPostfix == null ? "" : urlPostfix;
StringBuilder pwd = new StringBuilder(path.length() + pnames.length);
StringBuilder markup =
new StringBuilder( (pnames.length + 3 >> 1) * path.length()
+ pnames.length
* (17 + prefix.length() + postfix.length()));
int k = path.indexOf(pnames[0]);
if (path.lastIndexOf(sep, k) != -1) {
pwd.append('/');
markup.append(sep);
}
for (int i = 0; i < pnames.length; i++ ) {
pwd.append(uriEncodePath(pnames[i]));
if (isDir || i < pnames.length - 1) {
pwd.append('/');
}
markup.append(anchorLinkStart).append(prefix).append(pwd)
.append(postfix).append(closeQuotedTag).append(pnames[i])
.append(anchorEnd);
if (isDir || i < pnames.length - 1) {
markup.append(sep);
}
}
return markup.toString();
}
/**
* Normalize the given <var>path</var> to its canonical form. I.e. all
* separators (<var>sep</var>) are replaced with a slash ('/'), all
* double slashes are replaced by a single slash, all single dot path
* components (".") of the formed path are removed and all double dot path
* components (".." ) of the formed path are replaced with its parent or
* '/' if there is no parent.
* <p>
* So the difference to {@link File#getCanonicalPath()} is, that this method
* does not hit the disk (just string manipulation), resolves <var>path</var>
* always against '/' and thus always returns an absolute path, which may
* actually not exist, and which has a single trailing '/' if the given
* <var>path</var> ends with the given <var>sep</var>.
*
* @param path path to mangle. If not absolute or {@code null}, the
* current working directory is assumed to be '/'.
* @param sep file separator to use to crack <var>path</var> into path
* components
* @return always a canonical path which starts with a '/'.
*/
public static String getCanonicalPath(String path, char sep) {
if (path == null || path.length() == 0) {
return "/";
}
String[] pnames = normalize(path.split(escapeForRegex(sep)), true);
if (pnames.length == 0) {
return "/";
}
StringBuilder buf = new StringBuilder(path.length());
buf.append('/');
for (int i=0; i < pnames.length; i++) {
buf.append(pnames[i]).append('/');
}
if (path.charAt(path.length()-1) != sep) {
// since is not a general purpose method. So we waive to handle
// cases like:
// || path.endsWith("/..") || path.endsWith("/.")
buf.setLength(buf.length()-1);
}
return buf.toString();
}
private final static Pattern EMAIL_PATTERN =
Pattern.compile("([^<\\s]+@[^>\\s]+)");
/**
* Get email address of the author.
*
* @param author
* string containing author and possibly email address.
* @return the first email address found in the given parameter, the
* parameter otherwise. Returned values are whitespace trimmed.
*/
public static String getEmail(String author) {
Matcher email_matcher = EMAIL_PATTERN.matcher(author);
String email = author;
if (email_matcher.find()) {
email = email_matcher.group(1).trim();
}
return email.trim();
}
/**
* Remove all empty and {@code null} string elements from the given
* <var>names</var> and optionally all redundant information like "." and
* "..".
*
* @param names
* names to check
* @param canonical
* if {@code true}, remove redundant elements as well.
* @return a possible empty array of names all with a length &gt; 0.
*/
private static String[] normalize(String[] names, boolean canonical) {
LinkedList<String> res = new LinkedList<String>();
if (names == null || names.length == 0) {
return new String[0];
}
for (int i = 0; i < names.length; i++ ) {
if (names[i] == null || names[i].length() == 0) {
continue;
}
if (canonical) {
if (names[i].equals("..")) {
if (!res.isEmpty()) {
res.removeLast();
}
} else if (names[i].equals(".")) {
continue;
} else {
res.add(names[i]);
}
} else {
res.add(names[i]);
}
}
return res.size() == names.length ? names : res.toArray(new String[res
.size()]);
}
/**
* Generate a regex that matches the specified character. Escape it in case
* it is a character that has a special meaning in a regex.
*
* @param c
* the character that the regex should match
* @return a six-character string on the form <tt>&#92;u</tt><i>hhhh</i>
*/
private static String escapeForRegex(char c) {
StringBuilder sb = new StringBuilder(6);
sb.append("\\u");
String hex = Integer.toHexString(c);
for (int i = 0; i < 4 - hex.length(); i++ ) {
sb.append('0');
}
sb.append(hex);
return sb.toString();
}
private static NumberFormat FORMATTER = new DecimalFormat("#,###,###,###.#");
/**
* Convert the given size into a human readable string.
* @param num size to convert.
* @return a readable string
*/
public static String readableSize(long num) {
float l = num;
NumberFormat formatter = (NumberFormat) FORMATTER.clone();
if (l < 1024) {
return formatter.format(l) + ' '; // for none-dirs append 'B'? ...
} else if (l < 1048576) {
return (formatter.format(l / 1024) + " KiB");
} else {
return ("<b>" + formatter.format(l / 1048576) + " MiB</b>");
}
}
/**
* Generate a string from the given path and date in a way that allows
* stable lexicographic sorting (i.e. gives always the same results) as a
* walk of the file hierarchy. Thus null character (\u0000) is used both
* to separate directory components and to separate the path from the date.
* @param path path to mangle.
* @param date date string to use.
* @return the mangled path.
*/
public static String path2uid(String path, String date) {
return path.replace('/', '\u0000') + "\u0000" + date;
}
/**
* The reverse operation for {@link #path2uid(String, String)} - re-creates
* the unmangled path from the given uid.
* @param uid uid to unmangle.
* @return the original path.
*/
public static String uid2url(String uid) {
String url = uid.replace('\u0000', '/');
return url.substring(0, url.lastIndexOf('/')); // remove date from end
}
/**
* Append '&amp;name=value" properly URI encoded to the given buffer. If
* the given <var>value</var> is {@code null}, this method does nothing.
*
* @param buf where to append the query string
* @param key the name of the parameter to add. Append as is!
* @param value the decoded value for the given parameter.
* @throws NullPointerException if the given buffer is {@code null}.
* @see #uriEncodeQueryValue(String)
*/
public static void appendQuery(StringBuilder buf, String key,
String value)
{
if (value != null) {
buf.append("&amp;").append(key).append('=')
.append(uriEncodeQueryValue(value));
}
}
/* ASCII chars 0..63, which need to be escaped in an URI path. The segment
* delimiter '/' (47) is not added to this set, because it is used to
* encode file pathes which use the same delimiter for path components.
* Since we do not use any path parameters we add ';' (59) as well.
* Furthermore to avoid the need of a 2nd run to escape '&' for HTML
* attributes, we encode '&' (38) as well.
* RFC 3986: chars 0..32 34 35 37 60 62 63 */
private static final long PATH_ESCAPE_NC_L = 1L<<33-1 | 1L<<'"' | 1L<<'#'
| 1L<<'%' | 1L<<'<' | 1L<<'>' | 1L<<'?' | 1L<<';' | 1L<<'&';
/* RFC 3986: path-noscheme - ':' (58) in the first segment needs to be encoded */
private static final long PATH_ESCAPE_L = PATH_ESCAPE_NC_L | 1L<<':';
/* ASCII chars 64..127, which need to be escaped in an URI path.
* NOTE: URIs allow '~' unescaped, URLs not.
* RFC 3986: chars 91 92 93 94 96 123 124 125 127 */
private static final long PATH_ESCAPE_H = 1L<<'['-64 | 1L<<'\\'-64 | 1L<<']'-64
| 1L<<'^'-64 | 1L<<'`'-64 | 1L<<'{'-64 | 1L<<'|'-64 | 1L<<'}'-64 | 1L<<63;
/* ASCII chars 0..63, which need to be escaped in an URI query string as
* well as fragment part.
* RFC 3986: chars 32 34 35 37 60 62 */
private static final long QUERY_ESCAPE_L =
PATH_ESCAPE_NC_L ^ (1L<<'?' | 1L<<';' | 1L<<'&');
/* ASCII chars 64..127, which need to be escaped in an URI query string as
* well as fragment part.
* RFC 3986: chars 91 92 93 94 96 123 124 125 127 */
private static final long QUERY_ESCAPE_H = PATH_ESCAPE_H;
/* Query string starts at the left-most '?', ends at the next '#' or if not
* available at the end of the URI string and contains name/value pairs
* of the form $name['='[$value]] delimited by '&'. So no need to encode '='
* for query values, but for query names. */
private static final long QUERY_VALUE_ESCAPE_L = QUERY_ESCAPE_L | 1L<<'&';
private static final long QUERY_VALUE_ESCAPE_H = QUERY_ESCAPE_H;
private static final long QUERY_NAME_ESCAPE_L = QUERY_VALUE_ESCAPE_L | 1L<<'=';
private static final long QUERY_NAME_ESCAPE_H = QUERY_ESCAPE_H;
private static final long URL_ESCAPE_L = PATH_ESCAPE_NC_L ^ (1L<<'?' | 1L<<';');
private static final long URL_ESCAPE_H = PATH_ESCAPE_H ^ (1L<<'?' | 1L<<';');
/** Loose check, whether we have an entity reference: either #[0-9A-Fa-f]*;
* or [A-Za-z]*; */
private static boolean isEntityRef(String s, int start) {
int p = s.indexOf(';', start);
if (p == -1 || p - start > 8 || p - start < 2) {
return false;
}
char c = s.charAt(start);
if (c == '#') {
start++;
c = s.charAt(start);
if (c == 'x' || c == 'X') {
start++;
if (start == p) {
return false;
}
for (int i=start; i < p; i++) {
c = s.charAt(i);
if ( ! (('0' <= c && c <= '9') || ('A' <= c && c <= 'F')
|| ('a' <= c && c <= 'f')))
{
return false;
}
}
} else {
for (int i=start; i < p; i++) {
c = s.charAt(i);
if ( ! ('0' <= c && c <= '9')) {
return false;
}
}
}
return true;
}
for (int i=start+1; i < p; i++) {
c = s.charAt(i);
if ( ! (('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z'))) {
return false;
}
}
return true;
}
/**
* Tries to URI encode a URL. Difference to {@link #uriEncodePath(String)}
* is, that this method tries to find out, whether the URL already contains
* escaped characters and thus avoids double-encoding HTML special chars.
* NOTE: As soon as an entity reference is encountered, encoding stops and
* assumes an already encoded URL - the parameter gets returned as is!
* @param url URL to encode
* @return the URI encoded URL
*/
public static String uriEncodeURL(String url) {
if (url.length() == 0) {
return url;
}
int i=0;
for (;i < url.length(); i++) {
char c = url.charAt(i);
if (c < 64) {
if ((URL_ESCAPE_L & (1L << c)) != 0) {
break;
}
} else if (c < 0x80) {
if ((URL_ESCAPE_H & (1L << c-64)) != 0) {
break;
}
} else {
break;
}
}
if (i == url.length()) {
return url;
}
StringBuilder sb = new StringBuilder(url.length());
sb.ensureCapacity(url.length() + 24);
sb.append(url, 0, i);
for(; i < url.length(); i++) {
char c = url.charAt(i);
if (c < 64) {
if ((URL_ESCAPE_L & (1L << c)) != 0) {
if (c == '&') {
if (isEntityRef(url, i+1)) {
return url;
}
sb.append("&amp;");
} else {
appendEscape(sb, (byte) c);
}
} else {
sb.append(c);
}
} else if (c < 0x80) {
if ((URL_ESCAPE_H & (1L << c-64)) != 0) {
appendEscape(sb, (byte) c);
} else {
sb.append(c);
}
} else {
ByteBuffer bb = Charset.forName("UTF-8")
.encode(CharBuffer.wrap(url, i, url.length()));
while (bb.hasRemaining()) {
int b = bb.get() & 0xff;
if (b < 0x64) {
if ((URL_ESCAPE_L & (1L << b)) != 0) {
if (b == '&') {
if (isEntityRef(url, i+1)) {
return url;
}
sb.append("&amp;");
} else {
appendEscape(sb, (byte) b);
}
} else {
sb.append((char) b);
}
} else if (c < 0x80) {
if ((URL_ESCAPE_H & (1L << b-64)) != 0) {
appendEscape(sb, (byte) b);
} else {
sb.append((char) b);
}
} else {
appendEscape(sb, (byte) b);
}
i++;
}
break;
}
}
return sb.toString();
}
/**
* URI encode the given path using UTF-8 encoding for non-ASCII characters.
* In addition ';', '&' (and ':' for relative paths) get encoded as well,
* but not '/'. This method should not be used to encode query strings - for
* this {@link #uriEncodeQueryName(String)} and
* {@link #uriEncodeQueryValue(String)} should be used.
* NOTE: Since single quote (') gets not encoded, the encoded result should
* be enclosed in double quotes (") when used as attribute values!
*
* @param path path to encode.
* @return the encoded path.
* @throws NullPointerException if a parameter is {@code null}
* @see "http://tools.ietf.org/html/rfc3986"
*/
public static String uriEncodePath(String path) {
if (path.length() == 0) {
return path;
}
return uriEncode(path,
path.charAt(0) == '/' ? PATH_ESCAPE_NC_L : PATH_ESCAPE_L,
PATH_ESCAPE_H);
}
/**
* URI encode the given value for a URI query string pair using UTF-8
* encoding for non-ASCII characters ('&' gets encoded as well). It can also
* be used to encode URI fragments properly.
* NOTE: Since single quote (') gets not encoded, the encoded result should
* be enclosed in double quotes (") when used as attribute values!
* @param value value to encode.
* @return the encoded value.
* @throws NullPointerException if a parameter is {@code null}
* @see "http://tools.ietf.org/html/rfc3986"
* @see #uriEncodeQueryName(String)
*/
public static String uriEncodeQueryValue(String value) {
return uriEncode(value, QUERY_VALUE_ESCAPE_L, QUERY_VALUE_ESCAPE_H);
}
/**
* URI encode the given name for a URI query string pair using UTF-8
* encoding for non-ASCII characters ('=' and '&' get encoded as well).
* NOTE: Since single quote (') gets not encoded, the encoded result should
* be enclosed in double quotes (") when used as attribute values!
* @param name name to encode.
* @return the encoded name.
* @throws NullPointerException if a parameter is {@code null}
* @see "http://tools.ietf.org/html/rfc3986"
* @see #uriEncodeQueryValue(String)
*/
public static String uriEncodeQueryName(String name) {
return uriEncode(name, QUERY_NAME_ESCAPE_L, QUERY_NAME_ESCAPE_H);
}
/* int -> hex encoding helper */
private final static char[] hex = "0123456789ABCDEF".toCharArray();
private static void appendEscape(StringBuilder sb, byte b) {
sb.append('%').append(hex[b >> 4 & 0x0F]).append(hex[b & 0x0F]);
}
private static String uriEncode(String s, long lowMask, long hiMask) {
int i=0;
for (;i < s.length(); i++) {
char c = s.charAt(i);
if (c < 64) {
if ((lowMask & (1L << c)) != 0) {
break;
}
} else if (c < 0x80) {
if ((hiMask & (1L << c-64)) != 0) {
break;
}
} else {
break;
}
}
if (i == s.length()) {
return s;
}
StringBuilder sb = new StringBuilder(s.length() - i + 24);
sb.append(s, 0, i);
for(; i < s.length(); i++) {
char c = s.charAt(i);
if (c < 64) {
if ((lowMask & (1L << c)) != 0) {
appendEscape(sb, (byte) c);
} else {
sb.append(c);
}
} else if (c < 0x80) {
if ((hiMask & (1L << c-64)) != 0) {
appendEscape(sb, (byte) c);
} else {
sb.append(c);
}
} else {
ByteBuffer bb = Charset.forName("UTF-8")
.encode(CharBuffer.wrap(s, i, s.length()));
while (bb.hasRemaining()) {
int b = bb.get() & 0xff;
if (b < 0x64) {
if ((lowMask & (1L << b)) != 0) {
appendEscape(sb, (byte) b);
} else {
sb.append((char) b);
}
} else if (b < 0x80) {
if ((hiMask & (1L << b-64)) != 0) {
appendEscape(sb, (byte) b);
} else {
sb.append((char) b);
}
} else {
appendEscape(sb, (byte) b);
}
}
break;
}
}
return sb.toString();
}
/**
* Replace all quote and ampersand characters (ASCII 0x22, 0x26) with the
* corresponding html entity (&amp;quot; , &amp;amp;).
* @param q string to escape.
* @return an empty string if a parameter is {@code null}, the mangled
* string otherwise.
*/
public static String formQuoteEscape(String q) {
if (q == null || q.isEmpty()) {
return "";
}
char c;
int pos = -1;
for (int i = 0; i < q.length(); i++ ) {
c = q.charAt(i);
if (c == '"' || c == '&') {
pos = i;
break;
}
}
if (pos == -1) {
return q;
}
StringBuilder sb = new StringBuilder(q.substring(0, pos));
for (int i = pos; i < q.length(); i++ ) {
c = q.charAt(i);
if (c == '"') {
sb.append("&quot;");
} else if (c == '&') {
sb.append("&amp;");
} else {
sb.append(c);
}
}
return sb.toString();
}
/** span open tag incl. css class used to tag removed source code */
public static final String SPAN_D = "<span class=\"m\">";
/** span open tag incl. css class used to tag added source code */
public static final String SPAN_A = "<span class=\"p\">";
private static final String SPAN_E = "</span>";
private static final int SPAN_LEN = SPAN_D.length() + SPAN_E.length();
/**
* Tag changes in the given <var>line1</var> and <var>line2</var>
* for highlighting. Removed parts are tagged with CSS class {@code d},
* new parts are tagged with CSS class {@code a} using a {@code span}
* element.
*
* @param line1 line of the original file
* @param line2 line of the changed/new file
* @return the tagged lines (field[0] ~= line1, field[1] ~= line2).
* @throws NullPointerException if one of the given parameters is {@code null}.
*/
public static String[] diffline(String line1, String line2) {
int m = line1.length();
int n = line2.length();
if (n == 0 || m == 0) {
return new String[] {line1.toString(), line2.toString()};
}
int s = 0;
char[] csl1 = new char[m + SPAN_LEN];
line1.getChars(0, m--, csl1, 0);
char[] csl2 = new char[n + SPAN_LEN];
line2.getChars(0, n--, csl2, 0);
while (s <= m && s <= n && csl1[s] == csl2[s]) {
s++ ;
}
while (s <= m && s <= n && csl1[m] == csl2[n]) {
m-- ;
n-- ;
}
String[] ret = new String[2];
// deleted
if (s <= m) {
m++;
System.arraycopy(csl1, m, csl1, m + SPAN_LEN, line1.length() - m);
System.arraycopy(csl1, s, csl1, s + SPAN_D.length(), m - s);
SPAN_E.getChars(0, SPAN_E.length(), csl1, m + SPAN_D.length());
SPAN_D.getChars(0, SPAN_D.length(), csl1, s);
ret[0] = new String(csl1);
} else {
ret[0] = line1.toString();
}
// added
if (s <= n) {
n++;
System.arraycopy(csl2, n, csl2, n + SPAN_LEN, line2.length() - n);
System.arraycopy(csl2, s, csl2, s + SPAN_A.length(), n - s);
SPAN_E.getChars(0, SPAN_E.length(), csl2, n + SPAN_A.length());
SPAN_A.getChars(0, SPAN_A.length(), csl2, s);
ret[1] = new String(csl2);
} else {
ret[1] = line2.toString();
}
return ret;
}
/**
* Dump the configuration as an HTML table.
*
* @param out
* destination for the HTML output
* @throws IOException
* if an error happens while writing to {@code out}
* @throws HistoryException
* if the history guru cannot be accesses
*/
@SuppressWarnings("boxing")
public static void dumpConfiguration(Appendable out) throws IOException,
HistoryException
{
out.append("<table border=\"1\" width=\"100%\">");
out.append("<tr><th>Variable</th><th>Value</th></tr>");
Configuration cfg = RuntimeEnvironment.getConfig();
printTableRow(out, "Source root", cfg.getSourceRoot());
printTableRow(out, "Data root", cfg.getDataRoot());
printTableRow(out, "CTags", cfg.getCtags());
printTableRow(out, "Bug page", cfg.getBugPage());
printTableRow(out, "Bug pattern", cfg.getBugPattern());
printTableRow(out, "User page", cfg.getUserPage());
printTableRow(out, "User page suffix", cfg.getUserPageSuffix());
printTableRow(out, "Review page", cfg.getReviewPage());
printTableRow(out, "Review pattern", cfg.getReviewPattern());
printTableRow(out, "Using projects", cfg.hasProjects());
printTableRow(out, "Styles", cfg.getWebappLAF());
out.append("<tr><td>Ignored files</td><td>");
printUnorderedList(out, cfg.getIgnoredNames().getItems());
out.append("</td></tr>");
printTableRow(out, "Index word limit", cfg.getIndexWordLimit());
printTableRow(out, "Allow leading wildcard in search",
cfg.isAllowLeadingWildcard());
printTableRow(out, "History cache", HistoryGuru.getInstance()
.getCacheInfo());
printTableRow(out, "Version", Info.getVersion() + '@' + Info.getRevision()
+ " " + new Date(Info.getLastModified()));
printTableRow(out, "Last Modified", new Date(cfg.getLastModified()));
out.append("<tr><td>YUI modules</td><td id='yui_modules'></td></tr>");
out.append("</table>");
}
/**
* Just read the given source and dump as is to the given destination.
* Does nothing, if one or more of the parameters is {@code null}.
* @param out write destination
* @param in source to read
* @throws IOException as defined by the given reader/writer
* @throws NullPointerException if a parameter is {@code null}.
*/
public static void dump(Writer out, Reader in) throws IOException {
if (in == null || out == null) {
return;
}
char[] buf = new char[8192];
int len = 0;
while ((len = in.read(buf)) >= 0) {
out.write(buf, 0, len);
}
}
/**
* Silently dump a file to the given destination. All {@link IOException}s
* gets caught and logged, but not re-thrown.
*
* @param out dump destination
* @param dir directory, which should contains the file.
* @param filename the basename of the file to dump.
* @param compressed if {@code true} the denoted file is assumed to be
* gzipped.
* @return {@code true} on success (everything read and written).
* @throws NullPointerException if a parameter is {@code null}.
*/
public static boolean dump(Writer out, File dir, String filename,
boolean compressed)
{
return dump(out, new File(dir, filename), compressed);
}
/**
* Silently dump a file to the given destination. All {@link IOException}s
* gets caught and logged, but not re-thrown.
*
* @param out dump destination
* @param file file to dump.
* @param compressed if {@code true} the denoted file is assumed to be
* gzipped.
* @return {@code true} on success (everything read and written).
* @throws NullPointerException if a parameter is {@code null}.
*/
@SuppressWarnings("resource")
public static boolean dump(Writer out, File file, boolean compressed) {
if (!file.exists()) {
return false;
}
FileInputStream fis = null;
GZIPInputStream gis = null;
Reader in = null;
try {
if (compressed) {
fis = new FileInputStream(file);
gis = new GZIPInputStream(fis, 4096);
in = new InputStreamReader(gis);
} else {
in = new FileReader(file);
}
dump(out, in);
return true;
} catch(IOException e) {
logger.warning("An error occured while piping file '" + file + "': "
+ e.getMessage());
logger.log(Level.FINE, "dump", e);
} finally {
IOUtils.close(in);
IOUtils.close(gis);
IOUtils.close(fis);
}
return false;
}
/**
* Print a row in an HTML table.
*
* @param out
* destination for the HTML output
* @param cells
* the values to print in the cells of the row
* @throws IOException
* if an error happens while writing to {@code out}
*/
private static void printTableRow(Appendable out, Object... cells)
throws IOException
{
out.append("<tr>");
for (Object cell : cells) {
out.append("<td>");
String str = (cell == null) ? "null" : cell.toString();
out.append(htmlize(str, "<br/>"));
out.append("</td>");
}
out.append("</tr>");
}
/**
* Print an unordered list (HTML).
*
* @param out
* destination for the HTML output
* @param items
* the list items
* @throws IOException
* if an error happens while writing to {@code out}
*/
private static void printUnorderedList(Appendable out,
Collection<String> items) throws IOException
{
out.append("<ul>");
for (String item : items) {
out.append("<li>");
out.append(htmlize(item));
out.append("</li>");
}
out.append("</ul>");
}
/**
* Create a string literal for use in JavaScript functions.
*
* @param str the string to be represented by the literal
* @return a JavaScript string literal. {@code null} values are converted to ''.
*/
public static String jsStringLiteral(String str) {
if (str == null) {
return "\"\"";
}
StringBuilder sb = new StringBuilder();
sb.append('"');
for (int i = 0; i < str.length(); i++ ) {
char c = str.charAt(i);
switch (c) {
case '"':
sb.append("\\\"");
break;
case '\\':
sb.append("\\\\");
break;
case '\n':
sb.append("\\n");
break;
case '\r':
sb.append("\\r");
break;
default:
sb.append(c);
}
}
sb.append('"');
return sb.toString();
}
/**
* Convert the given array into JSON format and write it to the given stream.
* If <var>name</var> is given, the result is {@code name: [ value, ...]},
* otherwise {@code [ value, ... ]}.
* @param out where to write the json formatted array
* @param name name of the array. Might be {@code null}.
* @param values array to convert. {@code null} are converted to ''.
* @throws IOException
* @see #jsStringLiteral(String)
* @see #writeJsonArray(Writer, String, String[])
*/
public static void writeJsonArray(Writer out, String name,
Collection<String> values) throws IOException
{
// could just call writeJsonArray(out, name, values.toArray(new String[]))
// but this can be an issue for big collections - so we rather
// "duplicate" the code
if (name != null) {
out.write('"');
out.write(name);
out.write("\":");
}
out.write('[');
Iterator<String> i = values.iterator();
if (i.hasNext()) {
out.write(jsStringLiteral(i.next()));
while (i.hasNext()) {
out.write(',');
out.write(jsStringLiteral(i.next()));
}
}
out.write(']');
}
/**
* Convert the given array into JSON format and write it to the given stream.
* If <var>name</var> is given, the result is {@code name: [ value, ...]},
* otherwise {@code [ value, ... ]}.
* @param out where to write the json formatted array
* @param name name of the array. Might be {@code null}.
* @param values array to convert. {@code null} are converted to ''.
* @throws IOException
* @see #jsStringLiteral(String)
* @see #writeJsonArray(Writer, String, Collection)
*/
public static void writeJsonArray(Writer out, String name, String[] values) throws IOException {
if (name != null) {
out.write('"');
out.write(name);
out.write("\":");
}
out.write('[');
if (values.length > 0) {
out.write(jsStringLiteral(values[0]));
for (int i=1; i < values.length; i++) {
out.write(',');
out.write(jsStringLiteral(values[i]));
}
}
out.write(']');
}
}