/* * 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, 2017, Oracle and/or its affiliates. All rights reserved. * Portions Copyright 2011 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.UnsupportedEncodingException; import java.io.Writer; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLEncoder; import java.nio.charset.Charset; import java.text.DecimalFormat; import java.text.NumberFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.SortedSet; import java.util.TreeMap; import java.util.function.Function; 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 javax.servlet.http.HttpServletRequest; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.opensolaris.opengrok.Info; import org.opensolaris.opengrok.configuration.Group; import org.opensolaris.opengrok.configuration.Project; import org.opensolaris.opengrok.configuration.RuntimeEnvironment; import org.opensolaris.opengrok.configuration.messages.Message; import org.opensolaris.opengrok.history.Annotation; import org.opensolaris.opengrok.history.HistoryException; import org.opensolaris.opengrok.history.HistoryGuru; import org.opensolaris.opengrok.logger.LoggerFactory; /** * Class for useful functions. */ public final class Util { private static final Logger LOGGER = LoggerFactory.getLogger(Util.class); private static final Charset UTF8 = Charset.forName("UTF-8"); private static final String SPAN_D = "<span class=\"d\">"; private static final String SPAN_A = "<span class=\"a\">"; private static final String SPAN_E = "</span>"; private Util() { // singleton } /** * Return a string which represents a <code>CharSequence</code> in HTML. * * @param q a character sequence * @return a string representing the character sequence in HTML */ public static String htmlize(CharSequence q) { StringBuilder sb = new StringBuilder(q.length() * 2); try { htmlize(q, sb); } catch (IOException ioe) { // IOException cannot happen when the destination is a // StringBuilder. Wrap in an AssertionError so that callers // don't have to check for an IOException that should never // happen. throw new AssertionError("StringBuilder threw IOException", ioe); } return sb.toString(); } /** * Append a character sequence to the given destination whereby special * characters for HTML are escaped accordingly. * * @param q a character sequence to escape * @param dest where to append the character sequence to * @throws IOException if an error occurred when writing to {@code dest} */ public static void htmlize(CharSequence q, Appendable dest) throws IOException { for (int i = 0; i < q.length(); i++) { htmlize(q.charAt(i), dest); } } /** * Append a character array to the given destination whereby special * characters for HTML are escaped accordingly. * * @param cs characters to escape * @param length max. number of characters to append, starting from index 0. * @param dest where to append the character sequence to * @throws IOException if an error occurred when writing to {@code dest} */ public static void htmlize(char[] cs, int length, Appendable dest) throws IOException { int len = length; if (cs.length < length) { len = cs.length; } for (int i = 0; i < len; i++) { htmlize(cs[i], dest); } } /** * Append a character to the given destination whereby special characters * special for HTML are escaped accordingly. * * @param c the character to append * @param dest where to append the character to * @throws IOException if an error occurred when writing to {@code dest} */ private static void htmlize(char c, Appendable dest) throws IOException { switch (c) { case '&': dest.append("&"); break; case '>': dest.append(">"); break; case '<': dest.append("<"); break; case '\n': dest.append("<br/>"); break; default: dest.append(c); } } private static final String versionP = htmlize(Info.getRevision()); /** * used by BUI - CSS needs this parameter for proper cache refresh (per * changeset) in client browser TODO jel: but useless, since the page cached * anyway. * * @return html escaped version (hg changeset) */ public static String versionParameter() { return versionP; } /** * Convenience 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 anchorClassStart = "<a class=\""; 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 email address of the author or full author string if the author * string does not contain an email address. */ public static String getEmail(String author) { Matcher emailMatcher = EMAIL_PATTERN.matcher(author); String email = author; if (emailMatcher.find()) { email = emailMatcher.group(1).trim(); } return email; } /** * 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 > 0. */ private static String[] normalize(String[] names, boolean canonical) { LinkedList<String> res = new LinkedList<>(); if (names == null || names.length == 0) { return new String[0]; } for (String name : names) { if (name == null || name.length() == 0) { continue; } if (canonical) { if (name.equals("..")) { if (!res.isEmpty()) { res.removeLast(); } } else if (name.equals(".")) { } else { res.add(name); } } else { res.add(name); } } return res.size() == names.length ? names : res.toArray(new String[res .size()]); } /** * Generate a regexp that matches the specified character. Escape it in case * it is a character that has a special meaning in a regexp. * * @param c the character that the regexp should match * @return a six-character string on the form <tt>\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. * * NOTE: when changing the output of this function make sure to adapt the * jQuery tablesorter custom parsers in web/httpheader.jspf * * @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 if (l < 1073741824) { return ("<b>" + formatter.format(l / 1048576) + " MiB</b>"); } else { return ("<b>" + formatter.format(l / 1073741824) + " GiB</b>"); } } /** * Converts different HTML special characters into their encodings used in * html. * * @param s input text * @return encoded text for use in <a title=""> tag */ public static String encode(String s) { /** * Make sure that the buffer is long enough to contain the whole string * with the expanded special characters. We use 1.5*length as a * heuristic. */ StringBuilder sb = new StringBuilder((int) Math.max(10, s.length() * 1.5)); try { encode(s, sb); } catch (IOException ex) { // IOException cannot happen when the destination is a // StringBuilder. Wrap in an AssertionError so that callers // don't have to check for an IOException that should never // happen. throw new AssertionError("StringBuilder threw IOException", ex); } return sb.toString(); } /** * Converts different HTML special characters into their encodings used in * html. * * @param s input text * @param dest appendable destination for appending the encoded characters * @throws java.io.IOException */ public static void encode(String s, Appendable dest) throws IOException { for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); if (c > 127 || c == '"' || c == '<' || c == '>' || c == '&' || c == '\'') { // special html characters dest.append("&#").append("" + (int) c).append(";"); } else if (c == ' ') { // non breaking space dest.append(" "); } else if (c == '\t') { dest.append("    "); } else if (c == '\n') { // <br/> dest.append("<br/>"); } else { dest.append(c); } } } /** * Encode URL * * @param urlStr string URL * @return the encoded URL * @throws URISyntaxException * @throws MalformedURLException */ public static String encodeURL(String urlStr) throws URISyntaxException, MalformedURLException { URL url = new URL(urlStr); URI constructed = new URI(url.getProtocol(), url.getUserInfo(), url.getHost(), url.getPort(), url.getPath(), url.getQuery(), url.getRef()); return constructed.toString(); } /** * Write out line information wrt. to the given annotation in the format: * {@code Linenumber Blame Author} incl. appropriate links. * * @param num linenumber to print * @param out print destination * @param annotation annotation to use. If {@code null} only the linenumber * gets printed. * @param userPageLink see {@link RuntimeEnvironment#getUserPage()} * @param userPageSuffix see {@link RuntimeEnvironment#getUserPageSuffix()} * @param project project that is used * @throws IOException depends on the destination (<var>out</var>). */ public static void readableLine(int num, Writer out, Annotation annotation, String userPageLink, String userPageSuffix, String project) throws IOException { readableLine(num, out, annotation, userPageLink, userPageSuffix, project, false); } public static void readableLine(int num, Writer out, Annotation annotation, String userPageLink, String userPageSuffix, String project, boolean skipNewline) throws IOException { // this method should go to JFlexXref String snum = String.valueOf(num); if (num > 1 && !skipNewline) { out.write("\n"); } out.write(anchorClassStart); out.write(num % 10 == 0 ? "hl" : "l"); out.write("\" name=\""); out.write(snum); out.write("\" href=\"#"); out.write(snum); out.write(closeQuotedTag); out.write(snum); out.write(anchorEnd); if (annotation != null) { String r = annotation.getRevision(num); boolean enabled = annotation.isEnabled(num); out.write("<span class=\"blame\">"); if (enabled) { out.write(anchorClassStart); out.write("r"); if (annotation.getFileVersion(r) != 0) { /* version number, 1 is the most recent generates css classes version_color_n */ int versionNumber = Math.max(1, annotation.getFileVersionsCount() - annotation.getFileVersion(r) + 1); out.write(" version_color_" + versionNumber); } out.write("\" href=\""); out.write(URIEncode(annotation.getFilename())); out.write("?a=true&r="); out.write(URIEncode(r)); String msg = annotation.getDesc(r); out.write("\" title=\""); if (msg != null) { out.write(msg); } if (annotation.getFileVersion(r) != 0) { out.write("<br/>version: " + annotation.getFileVersion(r) + "/" + annotation.getRevisions().size()); } out.write(closeQuotedTag); } StringBuilder buf = new StringBuilder(); htmlize(r, buf); out.write(buf.toString()); buf.setLength(0); if (enabled) { RuntimeEnvironment env = RuntimeEnvironment.getInstance(); out.write(anchorEnd); // Write link to search the revision in current project. out.write(anchorClassStart); out.write("search\" href=\"" + env.getUrlPrefix()); out.write("defs=&refs=&path="); out.write(project); out.write("&hist=" + URIEncode(r)); out.write("&type=\" title=\"Search history for this changeset"); out.write(closeQuotedTag); out.write("S"); out.write(anchorEnd); } String a = annotation.getAuthor(num); if (userPageLink == null) { out.write("<span class=\"a\">"); htmlize(a, buf); out.write(buf.toString()); out.write("</span>"); buf.setLength(0); } else { out.write(anchorClassStart); out.write("a\" href=\""); out.write(userPageLink); out.write(URIEncode(a)); if (userPageSuffix != null) { out.write(userPageSuffix); } out.write(closeQuotedTag); htmlize(a, buf); out.write(buf.toString()); buf.setLength(0); out.write(anchorEnd); } out.write("</span>"); } } /** * 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 } /** * Write the 'H A D' links. This is used for search results and directory * listings. * * @param out writer for producing output * @param ctxE URI encoded prefix * @param entry file/directory name to write * @param is_dir is directory * @throws IOException depends on the destination (<var>out</var>). */ public static void writeHAD(Writer out, String ctxE, String entry, boolean is_dir) throws IOException { String histPrefixE = ctxE + Prefix.HIST_L; String downloadPrefixE = ctxE + Prefix.DOWNLOAD_P; String xrefPrefixE = ctxE + Prefix.XREF_P; out.write("<td class=\"q\"><a href=\""); out.write(histPrefixE); if (!entry.startsWith("/")) { entry = "/" + entry; } out.write(entry); out.write("\" title=\"History\">H</a>"); if (!is_dir) { out.write(" <a href=\""); out.write(xrefPrefixE); out.write(entry); out.write("?a=true\" title=\"Annotate\">A</a> "); out.write("<a href=\""); out.write(downloadPrefixE); out.write(entry); out.write("\" title=\"Download\">D</a>"); } out.write("</td>"); } /** * wrapper around UTF-8 URL encoding of a string * * @param q query to be encoded. If {@code null}, an empty string will be * used instead. * @return null if fail, otherwise the encoded string * @see URLEncoder#encode(String, String) */ public static String URIEncode(String q) { try { return q == null ? "" : URLEncoder.encode(q, "UTF-8"); } catch (UnsupportedEncodingException e) { // Should not happen. UTF-8 must be supported by JVMs. LOGGER.log( Level.WARNING, "Failed to URL-encode UTF-8: ", e); } return null; } /** * Append '&name=value" 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 value for the given parameter. Gets automatically UTF-8 * URL encoded. * @throws NullPointerException if the given buffer is {@code null}. * @see #URIEncode(String) */ public static void appendQuery(StringBuilder buf, String key, String value) { if (value != null) { buf.append("&").append(key).append('=').append(URIEncode(value)); } } /** * URI encode the given path. * * @param path path to encode. * @return the encoded path. * @throws NullPointerException if a parameter is {@code null} */ public static String URIEncodePath(String path) { // Bug #19188: Ideally, we'd like to use the standard class library to // encode the paths. We're aware of the following two methods: // // 1) URLEncoder.encode() - this method however transforms space to + // instead of %20 (which is right for HTML form data, but not for // paths), and it also does not preserve the separator chars (/). // // 2) URI.getRawPath() - transforms space and / as expected, but gets // confused when the path name contains a colon character (it thinks // parts of the path is schema in that case) // // For now, encode manually the way we want it. StringBuilder sb = new StringBuilder(path.length()); for (byte b : path.getBytes(UTF8)) { // URLEncoder's javadoc says a-z, A-Z, ., -, _ and * are safe // characters, so we preserve those to make the encoded string // shorter and easier to read. We also preserve the separator // chars (/). All other characters are encoded (as UTF-8 byte // sequences). if ((b == '/') || (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9') || (b == '.') || (b == '-') || (b == '_') || (b == '*')) { sb.append((char) b); } else { sb.append('%'); int u = b & 0xFF; // Make the byte value unsigned. if (u <= 0x0F) { // Add leading zero if required. sb.append('0'); } sb.append( Integer.toHexString(u).toUpperCase(Locale.ENGLISH)); } } return sb.toString(); } /** * Escape a string for use as in an HTML attribute value. The returned value * is not enclosed in double quotes. The caller needs to add those. * * @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 ""; } StringBuilder sb = new StringBuilder(); char c; for (int i = 0; i < q.length(); i++) { c = q.charAt(i); switch (c) { case '"': sb.append("""); break; case '&': sb.append("&"); break; default: sb.append(c); break; } } return sb.toString(); } /** * 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. * The input parameters must not have any HTML escapes in them. * * @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(StringBuilder line1, StringBuilder line2) { String[] ret = new String[2]; int s = 0; int m = line1.length() - 1; int n = line2.length() - 1; while (s <= m && s <= n && (line1.charAt(s) == line2.charAt(s))) { s++; } while (s <= m && s <= n && (line1.charAt(m) == line2.charAt(n))) { m--; n--; } // deleted if (s <= m) { StringBuilder sb = new StringBuilder(); sb.append(Util.htmlize(line1.substring(0, s))); sb.append(SPAN_D); sb.append(Util.htmlize(line1.substring(s, m + 1))); sb.append(SPAN_E); sb.append(Util.htmlize(line1.substring(m + 1, line1.length()))); ret[0] = sb.toString(); } else { ret[0] = line1.toString(); // no change } // added if (s <= n) { StringBuilder sb = new StringBuilder(); sb.append(Util.htmlize(line2.substring(0, s))); sb.append(SPAN_A); sb.append(Util.htmlize(line2.substring(s, n + 1))); sb.append(SPAN_E); sb.append(Util.htmlize(line2.substring(n + 1, line2.length()))); ret[1] = sb.toString(); } else { ret[1] = line2.toString(); // no change } 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>"); RuntimeEnvironment env = RuntimeEnvironment.getInstance(); printTableRow(out, "Source root", env.getSourceRootPath()); printTableRow(out, "Data root", env.getDataRootPath()); printTableRow(out, "CTags", env.getCtags()); printTableRow(out, "Bug page", env.getBugPage()); printTableRow(out, "Bug pattern", env.getBugPattern()); printTableRow(out, "User page", env.getUserPage()); printTableRow(out, "User page suffix", env.getUserPageSuffix()); printTableRow(out, "Review page", env.getReviewPage()); printTableRow(out, "Review pattern", env.getReviewPattern()); printTableRow(out, "Using projects", env.hasProjects()); out.append("<tr><td>Ignored files</td><td>"); printUnorderedList(out, env.getIgnoredNames().getItems()); out.append("</td></tr>"); printTableRow(out, "lucene RAM_BUFFER_SIZE_MB", env.getRamBufferSize()); printTableRow(out, "Allow leading wildcard in search", env.isAllowLeadingWildcard()); printTableRow(out, "History cache", HistoryGuru.getInstance() .getCacheInfo()); 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}. */ public static boolean dump(Writer out, File file, boolean compressed) { if (!file.exists()) { return false; } try (Reader in = compressed ? new InputStreamReader(new GZIPInputStream( new FileInputStream(file))) : new FileReader(file)) { dump(out, in); return true; } catch (IOException e) { LOGGER.log(Level.WARNING, "An error occured while piping file " + file + ": ", e); } return false; } /** * Print list of messages into output * * @param out output * @param set set of messages */ public static void printMessages(Writer out, SortedSet<Message> set) { printMessages(out, set, false); } /** * Print set of messages into output * * @param out output * @param set set of messages * @param limited if the container should be limited */ public static void printMessages(Writer out, SortedSet<Message> set, boolean limited) { SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z"); if (!set.isEmpty()) { try { out.write("<ul class=\"message-group"); if (limited) { out.write(" limited"); } out.write("\">\n"); for (Message m : set) { out.write("<li class=\"message-group-item "); out.write(Util.encode(m.getClassName())); out.write("\" title=\"Expires on "); out.write(Util.encode(df.format(m.getExpiration()))); out.write("\">"); out.write(Util.encode(df.format(m.getCreated()))); out.write(": "); out.write(m.getText()); out.write("</li>"); } out.write("</ul>"); } catch (IOException ex) { LOGGER.log(Level.WARNING, "An error occured for a group of messages", ex); } } } /** * Print set of messages into json array * * @param set set of messages * @return json array containing the set of messages */ @SuppressWarnings("unchecked") public static JSONArray messagesToJson(SortedSet<Message> set) { JSONArray array = new JSONArray(); SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z"); for (Message m : set) { JSONObject message = new JSONObject(); message.put("class", Util.encode(m.getClassName())); message.put("expiration", Util.encode(df.format(m.getExpiration()))); message.put("created", Util.encode(df.format(m.getCreated()))); message.put("text", Util.encode(m.getText())); JSONArray tags = new JSONArray(); for (String t : m.getTags()) { tags.add(Util.encode(t)); } message.put("tags", tags); array.add(message); } return array; } /** * Print set of messages into json object for given tag. * * @param tag return messages in json format for the given tag * @return json object with 'tag' and 'messages' attribute or null */ @SuppressWarnings("unchecked") public static JSONObject messagesToJsonObject(String tag) { SortedSet<Message> messages = RuntimeEnvironment.getInstance().getMessages(tag); if (messages.isEmpty()) { return null; } JSONObject toRet = new JSONObject(); toRet.put("tag", tag); toRet.put("messages", messagesToJson(messages)); return toRet; } /** * Print messages for given tags into json array * * @param array the array where the result should be stored * @param tags list of tags * @return json array of the messages (the same as the parameter) * @see #messagesToJsonObject(String) */ @SuppressWarnings("unchecked") public static JSONArray messagesToJson(JSONArray array, String... tags) { array = array == null ? new JSONArray() : array; for (String tag : tags) { JSONObject messages = messagesToJsonObject(tag); if (messages == null || messages.isEmpty()) { continue; } array.add(messages); } return array; } /** * Print messages for given tags into json array * * @param tags list of tags * @return json array of the messages * @see #messagesToJson(JSONArray, String...) * @see #messagesToJsonObject(String) */ public static JSONArray messagesToJson(String... tags) { return messagesToJson((JSONArray) null, tags); } /** * Print messages for given tags into json array * * @param tags list of tags * @return json array of the messages * @see #messagesToJson(String...) * @see #messagesToJsonObject(String) */ public static JSONArray messagesToJson(List<String> tags) { String[] array = new String[tags.size()]; return messagesToJson(tags.toArray(array)); } /** * Print messages for given project into json array. These messages are * tagged by project description or tagged by any of the project's group * name. * * @param project the project * @param additionalTags additional list of tags * @return the json array * @see #messagesToJson(String...) */ public static JSONArray messagesToJson(Project project, String... additionalTags) { if (project == null) { return new JSONArray(); } List<String> tags = new ArrayList<>(); tags.addAll(Arrays.asList(additionalTags)); tags.add(project.getName()); project.getGroups().stream().forEach((Group t) -> { tags.add(t.getName()); }); return messagesToJson(tags); } /** * Print messages for given project into json array. These messages are * tagged by project description or tagged by any of the project's group * name * * @param project the project * @return the json array * @see #messagesToJson(Project, String...) */ public static JSONArray messagesToJson(Project project) { return messagesToJson(project, new String[0]); } /** * Print messages for given group into json array. * * @param group the group * @param additionalTags additional list of tags * @return the json array * @see #messagesToJson(java.util.List) */ public static JSONArray messagesToJson(Group group, String... additionalTags) { List<String> tags = new ArrayList<>(); tags.add(group.getName()); tags.addAll(Arrays.asList(additionalTags)); return messagesToJson(tags); } /** * Print messages for given group into json array. * * @param group the group * @return the json array * @see #messagesToJson(Group, String...) */ public static JSONArray messagesToJson(Group group) { return messagesToJson(group, new String[0]); } /** * Convert statistics object into JSONObject. * * @param stats object containing statistics * @return the json object */ public static JSONObject statisticToJson(Statistics stats) { return stats.toJson(); } /** * Convert JSONObject object into statistics. * * @param input object containing statistics * @return the statistics object */ public static Statistics jsonToStatistics(JSONObject input) { return Statistics.from(input); } /** * 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>"); StringBuilder buf = new StringBuilder(256); for (Object cell : cells) { out.append("<td>"); String str = (cell == null) ? "null" : cell.toString(); htmlize(str, buf); out.append(str); buf.setLength(0); 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>"); StringBuilder buf = new StringBuilder(256); for (String item : items) { out.append("<li>"); htmlize(item, buf); out.append(buf); buf.setLength(0); 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 */ public static String jsStringLiteral(String str) { 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(); } /** * Make a path relative by stripping off a prefix. If the path does not have * the given prefix, return the full path unchanged. * * @param prefix the prefix to strip off * @param fullPath the path from which to remove the prefix * @return a path relative to {@code prefix} if {@code prefix} is a parent * directory of {@code fullPath}; otherwise, {@code fullPath} */ public static String stripPathPrefix(String prefix, String fullPath) { // Find the length of the prefix to strip off. The prefix should // represent a directory, so it could end with a slash. In case it // doesn't end with a slash, increase the length by one so that we // strip off the leading slash from the relative path. int prefixLength = prefix.length(); if (!prefix.endsWith("/")) { prefixLength++; } // If the full path starts with the prefix, strip off the prefix. if (fullPath.length() > prefixLength && fullPath.startsWith(prefix) && fullPath.charAt(prefixLength - 1) == '/') { return fullPath.substring(prefixLength); } // Otherwise, return the full path. return fullPath; } /** * Creates a html slider for pagination. This has the same effect as * invoking <code>createSlider(offset, limit, size, null)</code>. * * @param offset start of the current page * @param limit max number of items per page * @param size number of total hits to paginate * @return string containing slider html */ public static String createSlider(int offset, int limit, int size) { return createSlider(offset, limit, size, null); } /** * Creates a html slider for pagination. * * * @param offset start of the current page * @param limit max number of items per page * @param size number of total hits to paginate * @param request request containing URL parameters which should be appended * to the page URL * @return string containing slider html */ public static String createSlider(int offset, int limit, int size, HttpServletRequest request) { String slider = ""; if (limit < size) { final StringBuilder buf = new StringBuilder(4096); int lastPage = (int) Math.ceil((double) size / limit); // startingResult is the number of a first result on the current page int startingResult = offset - limit * (offset / limit % 10 + 1); int myFirstPage = startingResult < 0 ? 1 : startingResult / limit + 1; int myLastPage = Math.min(lastPage, myFirstPage + 10 + (myFirstPage == 1 ? 0 : 1)); // function taking the page number and appending the desired content into the final buffer Function<Integer, Void> generatePageLink = new Function<Integer, Void>() { @Override public Void apply(Integer page) { int myOffset = Math.max(0, (page - 1) * limit); if (myOffset <= offset && offset < myOffset + limit) { // do not generate anchor for current page buf.append("<span class=\"sel\">").append(page).append("</span>"); } else { buf.append("<a class=\"more\" href=\"?"); // append request parameters if (request != null && request.getQueryString() != null) { String query = request.getQueryString(); query = query.replaceFirst("(\\?|&|&|)n=\\d+", ""); query = query.replaceFirst("(\\?|&|&|)start=\\d+", ""); query = query.replaceFirst("^(\\?|&|&)", ""); if (!query.isEmpty()) { buf.append(query); buf.append("&"); } } buf.append("n=").append(limit); if (myOffset != 0) { buf.append("&start=").append(myOffset); } buf.append("\">"); // add << or >> if this link would lead to another section if (page == myFirstPage && page != 1) { buf.append("<<"); } else if (page == myLastPage && myOffset + limit < size) { buf.append(">>"); } else { buf.append(page); } buf.append("</a>"); } return null; } }; // slider composition if (myFirstPage != 1) { generatePageLink.apply(1); buf.append("<span>...</span>"); } for (int page = myFirstPage; page <= myLastPage; page++) { generatePageLink.apply(page); } if (myLastPage != lastPage) { buf.append("<span>...</span>"); generatePageLink.apply(lastPage); } return buf.toString(); } return slider; } /** * Check if the string is a http URL. * * @param string the string to check * @return true if it is http URL, false otherwise */ public static boolean isHttpUri(String string) { URL url; try { url = new URL(string); } catch (MalformedURLException ex) { return false; } return url.getProtocol().equals("http") || url.getProtocol().equals("https"); } /** * Build a html link to the given http url. If the URL is not an http URL * then it is returned as it was received. This has the same effect as * invoking <code>linkify(url, true)</code>. * * @param url the text to be linkified * @return the linkified string * * @see #linkify(java.lang.String, boolean) */ public static String linkify(String url) { return linkify(url, true); } /** * Build a html link to the given http URL. If the URL is not an http URL * then it is returned as it was received. * * @param url the http URL * @param newTab if the link should open in a new tab * @return html containing the link <a>...</a> */ public static String linkify(String url, boolean newTab) { if (isHttpUri(url)) { try { Map<String, String> attrs = new TreeMap<>(); attrs.put("href", url); attrs.put("title", String.format("Link to %s", Util.encode(url))); if (newTab) { attrs.put("target", "_blank"); } return buildLink(url, attrs); } catch (URISyntaxException | MalformedURLException ex) { return url; } } return url; } /** * Build an anchor with given name and a pack of attributes. Automatically * escapes href attributes and automatically escapes the name into HTML * entities. * * @param name displayed name of the anchor * @param attrs map of attributes for the html element * @return string containing the result * * @throws URISyntaxException * @throws MalformedURLException */ public static String buildLink(String name, Map<String, String> attrs) throws URISyntaxException, MalformedURLException { StringBuilder buffer = new StringBuilder(); buffer.append("<a"); for (Entry<String, String> attr : attrs.entrySet()) { buffer.append(" "); buffer.append(attr.getKey()); buffer.append("=\""); String value = attr.getValue(); if (attr.getKey().equals("href")) { value = Util.encodeURL(value); } buffer.append(value); buffer.append("\""); } buffer.append(">"); buffer.append(Util.htmlize(name)); buffer.append("</a>"); return buffer.toString(); } /** * Build an anchor with given name and a pack of attributes. Automatically * escapes href attributes and automatically escapes the name into HTML * entities. * * @param name displayed name of the anchor * @param url anchor's URL * @return string containing the result * * @throws URISyntaxException * @throws MalformedURLException */ public static String buildLink(String name, String url) throws URISyntaxException, MalformedURLException { Map<String, String> attrs = new TreeMap<>(); attrs.put("href", url); return buildLink(name, attrs); } /** * Build an anchor with given name and a pack of attributes. Automatically * escapes href attributes and automatically escapes the name into HTML * entities. * * @param name displayed name of the anchor * @param url anchor's URL * @param newTab a flag if the link should be opend in a new tab * @return string containing the result * * @throws URISyntaxException * @throws MalformedURLException */ public static String buildLink(String name, String url, boolean newTab) throws URISyntaxException, MalformedURLException { Map<String, String> attrs = new TreeMap<>(); attrs.put("href", url); if (newTab) { attrs.put("target", "_blank"); } return buildLink(name, attrs); } /** * Replace all occurrences of pattern in the incoming text with the link * named name pointing to an URL. It is possible to use the regexp pattern * groups in name and URL when they are specified in the pattern. * * @param text text to replace all patterns * @param pattern the pattern to match * @param name link display name * @param url link URL * @return the text with replaced links */ public static String linkifyPattern(String text, Pattern pattern, String name, String url) { try { String buildLink = buildLink(name, url, true); return pattern.matcher(text).replaceAll(buildLink); } catch (URISyntaxException | MalformedURLException ex) { LOGGER.log(Level.WARNING, "The given URL '{0}' is not valid", url); return text; } } /** * Try to complete the given URL part into full URL with server name, port, * scheme, ... * <dl> * <dt>for request http://localhost:8080/source/xref/xxx and part * /cgi-bin/user=</dt> * <dd>http://localhost:8080/cgi-bin/user=</dd> * <dt>for request http://localhost:8080/source/xref/xxx and part * cgi-bin/user=</dt> * <dd>http://localhost:8080/source/xref/xxx/cgi-bin/user=</dd> * <dt>for request http://localhost:8080/source/xref/xxx and part * http://users.com/user=</dt> * <dd>http://users.com/user=</dd> * </dl> * * @param url the given URL part, may be already full URL * @param req the request containing the information about the server * @return the converted URL or the input parameter if there was an error */ public static String completeUrl(String url, HttpServletRequest req) { try { if (!isHttpUri(url)) { if (url.startsWith("/")) { return new URI(req.getScheme(), null, req.getServerName(), req.getServerPort(), url, null, null).toString(); } StringBuffer prepUrl = req.getRequestURL(); if (!url.isEmpty()) { prepUrl.append('/').append(url); } return new URI(prepUrl.toString()).toString(); } return url; } catch (URISyntaxException ex) { LOGGER.log(Level.INFO, String.format("Unable to convert given URL part '%s' to complete URL", url), ex); return url; } } }