/* * 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 s 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("&"); break; case '>': sb.append(">"); break; case '<': sb.append("<"); 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 s 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("&"); break; case '>': sb.append(">"); break; case '<': sb.append("<"); 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 = ""; /** * 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 (sep) in the given path are * always collapsed into a single separator automatically. If * compact 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 * path * @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 sep to its name * @return path 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 path to its canonical form. I.e. all * separators (sep) 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. *

* So the difference to {@link File#getCanonicalPath()} is, that this method * does not hit the disk (just string manipulation), resolves path * always against '/' and thus always returns an absolute path, which may * actually not exist, and which has a single trailing '/' if the given * path ends with the given sep. * * @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 path 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 * names 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 > 0. */ private static String[] normalize(String[] names, boolean canonical) { LinkedList res = new LinkedList(); 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 \uhhhh */ 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 ("" + formatter.format(l / 1048576) + " MiB"); } } /** * 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 '&name=value" properly URI encoded to the given buffer. If * the given value 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("&").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("&"); } 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("&"); } 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 (&quot; , &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("""); } else if (c == '&') { sb.append("&"); } 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 open tag incl. css class used to tag added source code */ public static final String SPAN_A = ""; private static final String SPAN_E = ""; private static final int SPAN_LEN = SPAN_D.length() + SPAN_E.length(); /** * Tag changes in the given line1 and line2 * 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(""); out.append(""); 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(""); 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(""); out.append("
VariableValue
Ignored files"); printUnorderedList(out, cfg.getIgnoredNames().getItems()); out.append("
YUI modules
"); } /** * 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(""); for (Object cell : cells) { out.append(""); String str = (cell == null) ? "null" : cell.toString(); out.append(htmlize(str, "
")); out.append(""); } out.append(""); } /** * 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 items) throws IOException { out.append("

"); } /** * 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 name 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 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 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 name 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(']'); } }