xref: /OpenGrok/opengrok-indexer/src/main/java/org/opengrok/indexer/web/Util.java (revision d6df19e1b22784c78f567cf74c42f18e3901b900)
1b5840353SAdam Hornáček /*
2b5840353SAdam Hornáček  * CDDL HEADER START
3b5840353SAdam Hornáček  *
4b5840353SAdam Hornáček  * The contents of this file are subject to the terms of the
5b5840353SAdam Hornáček  * Common Development and Distribution License (the "License").
6b5840353SAdam Hornáček  * You may not use this file except in compliance with the License.
7b5840353SAdam Hornáček  *
8b5840353SAdam Hornáček  * See LICENSE.txt included in this distribution for the specific
9b5840353SAdam Hornáček  * language governing permissions and limitations under the License.
10b5840353SAdam Hornáček  *
11b5840353SAdam Hornáček  * When distributing Covered Code, include this CDDL HEADER in each
12b5840353SAdam Hornáček  * file and include the License file at LICENSE.txt.
13b5840353SAdam Hornáček  * If applicable, add the following below this CDDL HEADER, with the
14b5840353SAdam Hornáček  * fields enclosed by brackets "[]" replaced with your own identifying
15b5840353SAdam Hornáček  * information: Portions Copyright [yyyy] [name of copyright owner]
16b5840353SAdam Hornáček  *
17b5840353SAdam Hornáček  * CDDL HEADER END
18b5840353SAdam Hornáček  */
19b5840353SAdam Hornáček 
20b5840353SAdam Hornáček /*
21b13c5a0eSAdam Hornacek  * Copyright (c) 2005, 2021, Oracle and/or its affiliates. All rights reserved.
2251e20d51SAdam Hornáček  * Portions Copyright (c) 2011, Jens Elkner.
2351e20d51SAdam Hornáček  * Portions Copyright (c) 2017, 2020, Chris Fraire <cfraire@me.com>.
24e87b8f8cSKryštof Tulinger  * Portions Copyright (c) 2019, Krystof Tulinger <k.tulinger@seznam.cz>.
25b5840353SAdam Hornáček  */
269805b761SAdam Hornáček package org.opengrok.indexer.web;
27b5840353SAdam Hornáček 
28cf51835aSKryštof Tulinger import static org.opengrok.indexer.index.Indexer.PATH_SEPARATOR;
29cf51835aSKryštof Tulinger 
30b5840353SAdam Hornáček import java.io.BufferedInputStream;
31b5840353SAdam Hornáček import java.io.File;
32b5840353SAdam Hornáček import java.io.FileInputStream;
33b5840353SAdam Hornáček import java.io.IOException;
34b5840353SAdam Hornáček import java.io.InputStream;
35b5840353SAdam Hornáček import java.io.InputStreamReader;
36b5840353SAdam Hornáček import java.io.Reader;
37b5840353SAdam Hornáček import java.io.Writer;
38b5840353SAdam Hornáček import java.net.MalformedURLException;
39b5840353SAdam Hornáček import java.net.URI;
40b5840353SAdam Hornáček import java.net.URISyntaxException;
41b5840353SAdam Hornáček import java.net.URL;
42911e8af0SAdam Hornáček import java.net.URLDecoder;
43b5840353SAdam Hornáček import java.net.URLEncoder;
44b5840353SAdam Hornáček import java.nio.charset.StandardCharsets;
45b5840353SAdam Hornáček import java.text.DecimalFormat;
46b5840353SAdam Hornáček import java.text.NumberFormat;
47b5840353SAdam Hornáček import java.util.Collection;
48911e8af0SAdam Hornáček import java.util.HashMap;
49b5840353SAdam Hornáček import java.util.LinkedList;
50b5840353SAdam Hornáček import java.util.List;
51b5840353SAdam Hornáček import java.util.Locale;
52b5840353SAdam Hornáček import java.util.Map;
53b5840353SAdam Hornáček import java.util.Map.Entry;
54b5840353SAdam Hornáček import java.util.TreeMap;
55b5840353SAdam Hornáček import java.util.function.Function;
56b5840353SAdam Hornáček import java.util.logging.Level;
57b5840353SAdam Hornáček import java.util.logging.Logger;
58b5840353SAdam Hornáček import java.util.regex.Matcher;
59b5840353SAdam Hornáček import java.util.regex.Pattern;
60b5840353SAdam Hornáček import java.util.zip.GZIPInputStream;
619a4e74f4SAdam Hornacek 
629a4e74f4SAdam Hornacek import jakarta.servlet.http.HttpServletRequest;
636c62ede9SAdam Hornacek import org.apache.commons.lang3.SystemUtils;
64a5cf78b2SChris Fraire import org.apache.lucene.queryparser.classic.QueryParser;
659805b761SAdam Hornáček import org.opengrok.indexer.configuration.RuntimeEnvironment;
669805b761SAdam Hornáček import org.opengrok.indexer.history.Annotation;
679805b761SAdam Hornáček import org.opengrok.indexer.history.HistoryException;
689805b761SAdam Hornáček import org.opengrok.indexer.history.HistoryGuru;
699805b761SAdam Hornáček import org.opengrok.indexer.logger.LoggerFactory;
70b5840353SAdam Hornáček 
71b5840353SAdam Hornáček /**
72b5840353SAdam Hornáček  * Class for useful functions.
73b5840353SAdam Hornáček  */
74b5840353SAdam Hornáček public final class Util {
75b5840353SAdam Hornáček 
76b5840353SAdam Hornáček     private static final Logger LOGGER = LoggerFactory.getLogger(Util.class);
77b5840353SAdam Hornáček 
78b5840353SAdam Hornáček     private static final int BOLD_COUNT_THRESHOLD = 1000;
79b5840353SAdam Hornáček 
80807ead8fSLubos Kosco     private static final String anchorLinkStart = "<a href=\"";
81807ead8fSLubos Kosco     private static final String anchorClassStart = "<a class=\"";
82807ead8fSLubos Kosco     private static final String anchorEnd = "</a>";
83807ead8fSLubos Kosco     private static final String closeQuotedTag = "\">";
84807ead8fSLubos Kosco 
851c830032SChris Fraire     private static final String RE_Q_ESC_AMP_AMP = "\\?|&amp;|&";
861c830032SChris Fraire     private static final String RE_Q_E_A_A_COUNT_EQ_VAL = "(" + RE_Q_ESC_AMP_AMP + "|\\b)" +
871c830032SChris Fraire             QueryParameters.COUNT_PARAM_EQ + "\\d+";
881c830032SChris Fraire     private static final String RE_Q_E_A_A_START_EQ_VAL = "(" + RE_Q_ESC_AMP_AMP + "|\\b)" +
891c830032SChris Fraire             QueryParameters.START_PARAM_EQ + "\\d+";
901c830032SChris Fraire     private static final String RE_A_ANCHOR_Q_E_A_A = "^(" + RE_Q_ESC_AMP_AMP + ")";
911c830032SChris Fraire 
92a5cf78b2SChris Fraire     /** Private to enforce static. */
Util()93b5840353SAdam Hornáček     private Util() {
94b5840353SAdam Hornáček     }
95b5840353SAdam Hornáček 
96b5840353SAdam Hornáček     /**
97b5840353SAdam Hornáček      * Return a string that represents <code>s</code> in HTML by calling
98b5840353SAdam Hornáček      * {@link #htmlize(java.lang.CharSequence, java.lang.Appendable, boolean)}
99b5840353SAdam Hornáček      * with {@code s}, a transient {@link StringBuilder}, and {@code true}.
100b5840353SAdam Hornáček      * <p>
101b5840353SAdam Hornáček      * (N.b. if no special characters are present, {@code s} is returned as is,
102b5840353SAdam Hornáček      * without the expensive call.)
103b5840353SAdam Hornáček      *
104b5840353SAdam Hornáček      * @param s a defined string
105b5840353SAdam Hornáček      * @return a string representing the character sequence in HTML
106b5840353SAdam Hornáček      */
prehtmlize(String s)107b5840353SAdam Hornáček     public static String prehtmlize(String s) {
108a72324b1SAdam Hornáček         if (!needsHtmlize(s, true)) {
109a72324b1SAdam Hornáček             return s;
110a72324b1SAdam Hornáček         }
111b5840353SAdam Hornáček 
112b5840353SAdam Hornáček         StringBuilder sb = new StringBuilder(s.length() * 2);
113b5840353SAdam Hornáček         try {
114b5840353SAdam Hornáček             htmlize(s, sb, true);
115b5840353SAdam Hornáček         } catch (IOException ioe) {
116b5840353SAdam Hornáček             // IOException cannot happen when the destination is a
117b5840353SAdam Hornáček             // StringBuilder. Wrap in an AssertionError so that callers
118b5840353SAdam Hornáček             // don't have to check for an IOException that should never
119b5840353SAdam Hornáček             // happen.
120b5840353SAdam Hornáček             throw new AssertionError("StringBuilder threw IOException", ioe);
121b5840353SAdam Hornáček         }
122b5840353SAdam Hornáček         return sb.toString();
123b5840353SAdam Hornáček     }
124b5840353SAdam Hornáček 
125b5840353SAdam Hornáček     /**
126b5840353SAdam Hornáček      * Calls
127b5840353SAdam Hornáček      * {@link #htmlize(java.lang.CharSequence, java.lang.Appendable, boolean)}
128b5840353SAdam Hornáček      * with {@code q}, a transient {@link StringBuilder}, and {@code true}.
129b5840353SAdam Hornáček      * @param q a character sequence
130b5840353SAdam Hornáček      * @return a string representing the character sequence in HTML
131b5840353SAdam Hornáček      */
prehtmlize(CharSequence q)132b5840353SAdam Hornáček     public static String prehtmlize(CharSequence q) {
133b5840353SAdam Hornáček         StringBuilder sb = new StringBuilder(q.length() * 2);
134b5840353SAdam Hornáček         try {
135b5840353SAdam Hornáček             htmlize(q, sb, true);
136b5840353SAdam Hornáček         } catch (IOException ioe) {
137b5840353SAdam Hornáček             // IOException cannot happen when the destination is a
138b5840353SAdam Hornáček             // StringBuilder. Wrap in an AssertionError so that callers
139b5840353SAdam Hornáček             // don't have to check for an IOException that should never
140b5840353SAdam Hornáček             // happen.
141b5840353SAdam Hornáček             throw new AssertionError("StringBuilder threw IOException", ioe);
142b5840353SAdam Hornáček         }
143b5840353SAdam Hornáček         return sb.toString();
144b5840353SAdam Hornáček     }
145b5840353SAdam Hornáček 
146b5840353SAdam Hornáček     /**
147a5cf78b2SChris Fraire      * Append to {@code dest} the UTF-8 URL-encoded representation of the
148a5cf78b2SChris Fraire      * Lucene-escaped version of {@code str}.
149b5840353SAdam Hornáček      * @param str a defined instance
150b5840353SAdam Hornáček      * @param dest a defined target
15181b586e6SVladimir Kotal      * @throws IOException I/O exception
152b5840353SAdam Hornáček      */
qurlencode(String str, Appendable dest)153*d6df19e1SAdam Hornacek     public static void qurlencode(String str, Appendable dest) throws IOException {
154*d6df19e1SAdam Hornacek         uriEncode(QueryParser.escape(str), dest);
155b5840353SAdam Hornáček     }
156b5840353SAdam Hornáček 
157b5840353SAdam Hornáček     /**
158b5840353SAdam Hornáček      * Return a string that represents a <code>CharSequence</code> in HTML by
159b5840353SAdam Hornáček      * calling
160b5840353SAdam Hornáček      * {@link #htmlize(java.lang.CharSequence, java.lang.Appendable, boolean)}
161b5840353SAdam Hornáček      * with {@code s}, a transient {@link StringBuilder}, and {@code false}.
162b5840353SAdam Hornáček      * <p>
163b5840353SAdam Hornáček      * (N.b. if no special characters are present, {@code s} is returned as is,
164b5840353SAdam Hornáček      * without the expensive call.)
165b5840353SAdam Hornáček      *
166b5840353SAdam Hornáček      * @param s a defined string
167b5840353SAdam Hornáček      * @return a string representing the character sequence in HTML
168b5840353SAdam Hornáček      */
htmlize(String s)169b5840353SAdam Hornáček     public static String htmlize(String s) {
170a72324b1SAdam Hornáček         if (!needsHtmlize(s, false)) {
171a72324b1SAdam Hornáček             return s;
172a72324b1SAdam Hornáček         }
173b5840353SAdam Hornáček 
174b5840353SAdam Hornáček         StringBuilder sb = new StringBuilder(s.length() * 2);
175b5840353SAdam Hornáček         try {
176b5840353SAdam Hornáček             htmlize(s, sb, false);
177b5840353SAdam Hornáček         } catch (IOException ioe) {
178b5840353SAdam Hornáček             // IOException cannot happen when the destination is a
179b5840353SAdam Hornáček             // StringBuilder. Wrap in an AssertionError so that callers
180b5840353SAdam Hornáček             // don't have to check for an IOException that should never
181b5840353SAdam Hornáček             // happen.
182b5840353SAdam Hornáček             throw new AssertionError("StringBuilder threw IOException", ioe);
183b5840353SAdam Hornáček         }
184b5840353SAdam Hornáček         return sb.toString();
185b5840353SAdam Hornáček     }
186b5840353SAdam Hornáček 
187b5840353SAdam Hornáček     /**
188b5840353SAdam Hornáček      * Return a string which represents a <code>CharSequence</code> in HTML by
189b5840353SAdam Hornáček      * calling
190b5840353SAdam Hornáček      * {@link #htmlize(java.lang.CharSequence, java.lang.Appendable, boolean)}
191b5840353SAdam Hornáček      * with {@code q}, a transient {@link StringBuilder}, and {@code false}.
192b5840353SAdam Hornáček      *
193b5840353SAdam Hornáček      * @param q a character sequence
194b5840353SAdam Hornáček      * @return a string representing the character sequence in HTML
195b5840353SAdam Hornáček      */
htmlize(CharSequence q)196b5840353SAdam Hornáček     public static String htmlize(CharSequence q) {
197b5840353SAdam Hornáček         StringBuilder sb = new StringBuilder(q.length() * 2);
198b5840353SAdam Hornáček         try {
199b5840353SAdam Hornáček             htmlize(q, sb, false);
200b5840353SAdam Hornáček         } catch (IOException ioe) {
201b5840353SAdam Hornáček             // IOException cannot happen when the destination is a
202b5840353SAdam Hornáček             // StringBuilder. Wrap in an AssertionError so that callers
203b5840353SAdam Hornáček             // don't have to check for an IOException that should never
204b5840353SAdam Hornáček             // happen.
205b5840353SAdam Hornáček             throw new AssertionError("StringBuilder threw IOException", ioe);
206b5840353SAdam Hornáček         }
207b5840353SAdam Hornáček         return sb.toString();
208b5840353SAdam Hornáček     }
209b5840353SAdam Hornáček 
210b5840353SAdam Hornáček     /**
211b5840353SAdam Hornáček      * Append a character sequence to the given destination whereby special
212b5840353SAdam Hornáček      * characters for HTML or characters that are not printable ASCII are
213b5840353SAdam Hornáček      * escaped accordingly.
214b5840353SAdam Hornáček      *
215b5840353SAdam Hornáček      * @param q a character sequence to escape
216b5840353SAdam Hornáček      * @param dest where to append the character sequence to
217b5840353SAdam Hornáček      * @param pre a value indicating whether the output is pre-formatted -- if
218b5840353SAdam Hornáček      * true then LFs will not be converted to &lt;br&gt; elements
219b5840353SAdam Hornáček      * @throws IOException if an error occurred when writing to {@code dest}
220b5840353SAdam Hornáček      */
htmlize(CharSequence q, Appendable dest, boolean pre)221b5840353SAdam Hornáček     public static void htmlize(CharSequence q, Appendable dest, boolean pre)
222b5840353SAdam Hornáček             throws IOException {
223b5840353SAdam Hornáček         for (int i = 0; i < q.length(); i++) {
224b5840353SAdam Hornáček             htmlize(q.charAt(i), dest, pre);
225b5840353SAdam Hornáček         }
226b5840353SAdam Hornáček     }
227b5840353SAdam Hornáček 
228b5840353SAdam Hornáček     /**
229b5840353SAdam Hornáček      * Calls
230b5840353SAdam Hornáček      * {@link #htmlize(java.lang.CharSequence, java.lang.Appendable, boolean)}
231b5840353SAdam Hornáček      * with {@code q}, {@code dest}, and {@code false}.
232b5840353SAdam Hornáček      *
233b5840353SAdam Hornáček      * @param q a character sequence to escape
234b5840353SAdam Hornáček      * @param dest where to append the character sequence to
235b5840353SAdam Hornáček      * @throws IOException if an error occurred when writing to {@code dest}
236b5840353SAdam Hornáček      */
htmlize(CharSequence q, Appendable dest)237b5840353SAdam Hornáček     public static void htmlize(CharSequence q, Appendable dest)
238b5840353SAdam Hornáček             throws IOException {
239b5840353SAdam Hornáček         htmlize(q, dest, false);
240b5840353SAdam Hornáček     }
241b5840353SAdam Hornáček 
242b5840353SAdam Hornáček     /**
243b5840353SAdam Hornáček      * Append a character array to the given destination whereby special
244b5840353SAdam Hornáček      * characters for HTML or characters that are not printable ASCII are
245b5840353SAdam Hornáček      * escaped accordingly.
246b5840353SAdam Hornáček      *
247b5840353SAdam Hornáček      * @param cs characters to escape
248b5840353SAdam Hornáček      * @param length max. number of characters to append, starting from index 0.
249b5840353SAdam Hornáček      * @param dest where to append the character sequence to
250b5840353SAdam Hornáček      * @throws IOException if an error occurred when writing to {@code dest}
251b5840353SAdam Hornáček      */
htmlize(char[] cs, int length, Appendable dest)252b5840353SAdam Hornáček     public static void htmlize(char[] cs, int length, Appendable dest)
253b5840353SAdam Hornáček             throws IOException {
254b5840353SAdam Hornáček         int len = length;
255b5840353SAdam Hornáček         if (cs.length < length) {
256b5840353SAdam Hornáček             len = cs.length;
257b5840353SAdam Hornáček         }
258b5840353SAdam Hornáček         for (int i = 0; i < len; i++) {
259b5840353SAdam Hornáček             htmlize(cs[i], dest, false);
260b5840353SAdam Hornáček         }
261b5840353SAdam Hornáček     }
262b5840353SAdam Hornáček 
263b5840353SAdam Hornáček     /**
264b5840353SAdam Hornáček      * Append a character to the given destination whereby special characters
265b5840353SAdam Hornáček      * special for HTML or characters that are not printable ASCII are
266b5840353SAdam Hornáček      * escaped accordingly.
267b5840353SAdam Hornáček      *
268b5840353SAdam Hornáček      * @param c the character to append
269b5840353SAdam Hornáček      * @param dest where to append the character to
270b5840353SAdam Hornáček      * @param pre a value indicating whether the output is pre-formatted -- if
271b5840353SAdam Hornáček      * true then LFs will not be converted to &lt;br&gt; elements
272b5840353SAdam Hornáček      * @throws IOException if an error occurred when writing to {@code dest}
273b5840353SAdam Hornáček      * @see #needsHtmlize(char, boolean)
274b5840353SAdam Hornáček      */
htmlize(char c, Appendable dest, boolean pre)275b5840353SAdam Hornáček     private static void htmlize(char c, Appendable dest, boolean pre)
276b5840353SAdam Hornáček             throws IOException {
277b5840353SAdam Hornáček         switch (c) {
278b5840353SAdam Hornáček             case '\'':
279b5840353SAdam Hornáček                 dest.append("&apos;");
280b5840353SAdam Hornáček                 break;
281b5840353SAdam Hornáček             case '"':
282b5840353SAdam Hornáček                 dest.append("&quot;");
283b5840353SAdam Hornáček                 break;
284b5840353SAdam Hornáček             case '&':
285b5840353SAdam Hornáček                 dest.append("&amp;");
286b5840353SAdam Hornáček                 break;
287b5840353SAdam Hornáček             case '>':
288b5840353SAdam Hornáček                 dest.append("&gt;");
289b5840353SAdam Hornáček                 break;
290b5840353SAdam Hornáček             case '<':
291b5840353SAdam Hornáček                 dest.append("&lt;");
292b5840353SAdam Hornáček                 break;
293b5840353SAdam Hornáček             case '\n':
294b5840353SAdam Hornáček                 if (pre) {
295b5840353SAdam Hornáček                     dest.append(c);
296b5840353SAdam Hornáček                 } else {
297b5840353SAdam Hornáček                     dest.append("<br/>");
298b5840353SAdam Hornáček                 }
299b5840353SAdam Hornáček                 break;
300b5840353SAdam Hornáček             default:
301b5840353SAdam Hornáček                 if ((c >= ' ' && c <= '~') || (c < ' ' &&
302b5840353SAdam Hornáček                     Character.isWhitespace(c))) {
303b5840353SAdam Hornáček                     dest.append(c);
304b5840353SAdam Hornáček                 } else {
305b5840353SAdam Hornáček                     dest.append("&#").append(Integer.toString(c)).append(';');
306b5840353SAdam Hornáček                 }
307b5840353SAdam Hornáček                 break;
308b5840353SAdam Hornáček         }
309b5840353SAdam Hornáček     }
310b5840353SAdam Hornáček 
311b5840353SAdam Hornáček     /**
312b5840353SAdam Hornáček      * Determine if a character is a special character needing HTML escaping or
313b5840353SAdam Hornáček      * is a character that is not printable ASCII.
314b5840353SAdam Hornáček      * @param c the character to examine
315b5840353SAdam Hornáček      * @param pre a value indicating whether the output is pre-formatted -- if
316b5840353SAdam Hornáček      * true then LFs will not be converted to &lt;br&gt; elements
317b5840353SAdam Hornáček      * @see #htmlize(char, java.lang.Appendable, boolean)
318b5840353SAdam Hornáček      */
needsHtmlize(char c, boolean pre)319b5840353SAdam Hornáček     private static boolean needsHtmlize(char c, boolean pre) {
320b5840353SAdam Hornáček         switch (c) {
321b5840353SAdam Hornáček             case '\'':
322b5840353SAdam Hornáček             case '"':
323b5840353SAdam Hornáček             case '&':
324b5840353SAdam Hornáček             case '>':
325b5840353SAdam Hornáček             case '<':
326b5840353SAdam Hornáček                 return true;
327b5840353SAdam Hornáček             case '\n':
328a72324b1SAdam Hornáček                 if (!pre) {
329a72324b1SAdam Hornáček                     return true;
330a72324b1SAdam Hornáček                 }
331b5840353SAdam Hornáček             default:
332b13c5a0eSAdam Hornacek                 return (c < ' ' || c > '~') && (c >= ' ' || !Character.isWhitespace(c));
333b5840353SAdam Hornáček         }
334b5840353SAdam Hornáček     }
335b5840353SAdam Hornáček 
needsHtmlize(CharSequence q, boolean pre)336b5840353SAdam Hornáček     private static boolean needsHtmlize(CharSequence q, boolean pre) {
337b5840353SAdam Hornáček         for (int i = 0; i < q.length(); ++i) {
338a72324b1SAdam Hornáček             if (needsHtmlize(q.charAt(i), pre)) {
339a72324b1SAdam Hornáček                 return true;
340a72324b1SAdam Hornáček             }
341b5840353SAdam Hornáček         }
342b5840353SAdam Hornáček         return false;
343b5840353SAdam Hornáček     }
344b5840353SAdam Hornáček 
345b5840353SAdam Hornáček     /**
346807ead8fSLubos Kosco      * Convenience method for {@code breadcrumbPath(urlPrefix, path, PATH_SEPARATOR)}.
347b5840353SAdam Hornáček      *
348b5840353SAdam Hornáček      * @param urlPrefix prefix to add to each url
349b5840353SAdam Hornáček      * @param path path to crack
350379f387bSVladimir Kotal      * @return HTML markup for the breadcrumb or the path itself.
351b5840353SAdam Hornáček      *
352b5840353SAdam Hornáček      * @see #breadcrumbPath(String, String, char)
353b5840353SAdam Hornáček      */
breadcrumbPath(String urlPrefix, String path)354b5840353SAdam Hornáček     public static String breadcrumbPath(String urlPrefix, String path) {
355807ead8fSLubos Kosco         return breadcrumbPath(urlPrefix, path, PATH_SEPARATOR);
356b5840353SAdam Hornáček     }
357b5840353SAdam Hornáček 
358b5840353SAdam Hornáček     /**
359b5840353SAdam Hornáček      * Convenience method for
360b5840353SAdam Hornáček      * {@code breadcrumbPath(urlPrefix, path, sep, "", false)}.
361b5840353SAdam Hornáček      *
362b5840353SAdam Hornáček      * @param urlPrefix prefix to add to each url
363b5840353SAdam Hornáček      * @param path path to crack
364b5840353SAdam Hornáček      * @param sep separator to use to crack the given path
365b5840353SAdam Hornáček      *
366b5840353SAdam Hornáček      * @return HTML markup fro the breadcrumb or the path itself.
367b5840353SAdam Hornáček      * @see #breadcrumbPath(String, String, char, String, boolean, boolean)
368b5840353SAdam Hornáček      */
breadcrumbPath(String urlPrefix, String path, char sep)369b5840353SAdam Hornáček     public static String breadcrumbPath(String urlPrefix, String path, char sep) {
370b5840353SAdam Hornáček         return breadcrumbPath(urlPrefix, path, sep, "", false);
371b5840353SAdam Hornáček     }
372b5840353SAdam Hornáček 
373b5840353SAdam Hornáček     /**
374b5840353SAdam Hornáček      * Convenience method for
375b5840353SAdam Hornáček      * {@code breadcrumbPath(urlPrefix, path, sep, "", false, path.endsWith(sep)}.
376b5840353SAdam Hornáček      *
377b5840353SAdam Hornáček      * @param urlPrefix prefix to add to each url
378b5840353SAdam Hornáček      * @param path path to crack
379b5840353SAdam Hornáček      * @param sep separator to use to crack the given path
380b5840353SAdam Hornáček      * @param urlPostfix suffix to add to each url
381b5840353SAdam Hornáček      * @param compact if {@code true} the given path gets transformed into its
382b5840353SAdam Hornáček      * canonical form (.i.e. all '.' and '..' and double separators removed, but
383b5840353SAdam Hornáček      * not always resolves to an absolute path) before processing starts.
384b5840353SAdam Hornáček      * @return HTML markup fro the breadcrumb or the path itself.
385b5840353SAdam Hornáček      * @see #breadcrumbPath(String, String, char, String, boolean, boolean)
386b5840353SAdam Hornáček      * @see #getCanonicalPath(String, char)
387b5840353SAdam Hornáček      */
breadcrumbPath(String urlPrefix, String path, char sep, String urlPostfix, boolean compact)388b5840353SAdam Hornáček     public static String breadcrumbPath(String urlPrefix, String path,
389b5840353SAdam Hornáček             char sep, String urlPostfix, boolean compact) {
390b5840353SAdam Hornáček         if (path == null || path.length() == 0) {
391b5840353SAdam Hornáček             return path;
392b5840353SAdam Hornáček         }
393b5840353SAdam Hornáček         return breadcrumbPath(urlPrefix, path, sep, urlPostfix, compact,
394b5840353SAdam Hornáček                 path.charAt(path.length() - 1) == sep);
395b5840353SAdam Hornáček     }
396b5840353SAdam Hornáček 
397b5840353SAdam Hornáček     /**
398b5840353SAdam Hornáček      * Create a breadcrumb path to allow navigation to each element of a path.
399b5840353SAdam Hornáček      * Consecutive separators (<var>sep</var>) in the given <var>path</var> are
400b5840353SAdam Hornáček      * always collapsed into a single separator automatically. If
401b5840353SAdam Hornáček      * <var>compact</var> is {@code true} path gets translated into a canonical
402b5840353SAdam Hornáček      * path similar to {@link File#getCanonicalPath()}, however the current
403b5840353SAdam Hornáček      * working directory is assumed to be "/" and no checks are done (e.g.
404b5840353SAdam Hornáček      * neither whether the path [component] exists nor which type it is).
405b5840353SAdam Hornáček      *
406b5840353SAdam Hornáček      * @param urlPrefix what should be prepend to the constructed URL
407b5840353SAdam Hornáček      * @param path the full path from which the breadcrumb path is built.
408b5840353SAdam Hornáček      * @param sep the character that separates the path components in
409b5840353SAdam Hornáček      * <var>path</var>
410b5840353SAdam Hornáček      * @param urlPostfix what should be append to the constructed URL
411b5840353SAdam Hornáček      * @param compact if {@code true}, a canonical path gets constructed before
412b5840353SAdam Hornáček      * processing.
413b5840353SAdam Hornáček      * @param isDir if {@code true} a "/" gets append to the last path
414b5840353SAdam Hornáček      * component's link and <var>sep</var> to its name
415b5840353SAdam Hornáček      * @return <var>path</var> if it resolves to an empty or "/" or {@code null}
416b5840353SAdam Hornáček      * path, the HTML markup for the breadcrumb path otherwise.
417b5840353SAdam Hornáček      */
breadcrumbPath(String urlPrefix, String path, char sep, String urlPostfix, boolean compact, boolean isDir)418b5840353SAdam Hornáček     public static String breadcrumbPath(String urlPrefix, String path,
419b5840353SAdam Hornáček             char sep, String urlPostfix, boolean compact, boolean isDir) {
420b5840353SAdam Hornáček         if (path == null || path.length() == 0) {
421b5840353SAdam Hornáček             return path;
422b5840353SAdam Hornáček         }
423b5840353SAdam Hornáček         String[] pnames = normalize(path.split(escapeForRegex(sep)), compact);
424b5840353SAdam Hornáček         if (pnames.length == 0) {
425b5840353SAdam Hornáček             return path;
426b5840353SAdam Hornáček         }
427b5840353SAdam Hornáček         String prefix = urlPrefix == null ? "" : urlPrefix;
428b5840353SAdam Hornáček         String postfix = urlPostfix == null ? "" : urlPostfix;
429b5840353SAdam Hornáček         StringBuilder pwd = new StringBuilder(path.length() + pnames.length);
430b5840353SAdam Hornáček         StringBuilder markup
431b5840353SAdam Hornáček                 = new StringBuilder((pnames.length + 3 >> 1) * path.length()
432b5840353SAdam Hornáček                         + pnames.length
433b5840353SAdam Hornáček                         * (17 + prefix.length() + postfix.length()));
434b5840353SAdam Hornáček         int k = path.indexOf(pnames[0]);
435b5840353SAdam Hornáček         if (path.lastIndexOf(sep, k) != -1) {
436807ead8fSLubos Kosco             pwd.append(PATH_SEPARATOR);
437b5840353SAdam Hornáček             markup.append(sep);
438b5840353SAdam Hornáček         }
439b5840353SAdam Hornáček         for (int i = 0; i < pnames.length; i++) {
440*d6df19e1SAdam Hornacek             pwd.append(uriEncodePath(pnames[i]));
441b5840353SAdam Hornáček             if (isDir || i < pnames.length - 1) {
442807ead8fSLubos Kosco                 pwd.append(PATH_SEPARATOR);
443b5840353SAdam Hornáček             }
444b5840353SAdam Hornáček             markup.append(anchorLinkStart).append(prefix).append(pwd)
445b5840353SAdam Hornáček                     .append(postfix).append(closeQuotedTag).append(pnames[i])
446b5840353SAdam Hornáček                     .append(anchorEnd);
447b5840353SAdam Hornáček             if (isDir || i < pnames.length - 1) {
448b5840353SAdam Hornáček                 markup.append(sep);
449b5840353SAdam Hornáček             }
450b5840353SAdam Hornáček         }
451b5840353SAdam Hornáček         return markup.toString();
452b5840353SAdam Hornáček     }
453b5840353SAdam Hornáček 
454b5840353SAdam Hornáček     /**
455b5840353SAdam Hornáček      * Normalize the given <var>path</var> to its canonical form. I.e. all
456b5840353SAdam Hornáček      * separators (<var>sep</var>) are replaced with a slash ('/'), all double
457b5840353SAdam Hornáček      * slashes are replaced by a single slash, all single dot path components
458b5840353SAdam Hornáček      * (".") of the formed path are removed and all double dot path components
459b5840353SAdam Hornáček      * (".." ) of the formed path are replaced with its parent or '/' if there
460b5840353SAdam Hornáček      * is no parent.
461b5840353SAdam Hornáček      * <p>
462b5840353SAdam Hornáček      * So the difference to {@link File#getCanonicalPath()} is, that this method
463b5840353SAdam Hornáček      * does not hit the disk (just string manipulation), resolves
464b5840353SAdam Hornáček      * <var>path</var>
465b5840353SAdam Hornáček      * always against '/' and thus always returns an absolute path, which may
466b5840353SAdam Hornáček      * actually not exist, and which has a single trailing '/' if the given
467b5840353SAdam Hornáček      * <var>path</var> ends with the given <var>sep</var>.
468b5840353SAdam Hornáček      *
469b5840353SAdam Hornáček      * @param path path to mangle. If not absolute or {@code null}, the current
470b5840353SAdam Hornáček      * working directory is assumed to be '/'.
471b5840353SAdam Hornáček      * @param sep file separator to use to crack <var>path</var> into path
472b5840353SAdam Hornáček      * components
473b5840353SAdam Hornáček      * @return always a canonical path which starts with a '/'.
474b5840353SAdam Hornáček      */
getCanonicalPath(String path, char sep)475b5840353SAdam Hornáček     public static String getCanonicalPath(String path, char sep) {
476b5840353SAdam Hornáček         if (path == null || path.length() == 0) {
477b5840353SAdam Hornáček             return "/";
478b5840353SAdam Hornáček         }
479b5840353SAdam Hornáček         String[] pnames = normalize(path.split(escapeForRegex(sep)), true);
480b5840353SAdam Hornáček         if (pnames.length == 0) {
481b5840353SAdam Hornáček             return "/";
482b5840353SAdam Hornáček         }
483b5840353SAdam Hornáček         StringBuilder buf = new StringBuilder(path.length());
484b5840353SAdam Hornáček         buf.append('/');
485b13c5a0eSAdam Hornacek         for (String pname : pnames) {
486b13c5a0eSAdam Hornacek             buf.append(pname).append('/');
487b5840353SAdam Hornáček         }
488b5840353SAdam Hornáček         if (path.charAt(path.length() - 1) != sep) {
489b5840353SAdam Hornáček             // since is not a general purpose method. So we waive to handle
490b5840353SAdam Hornáček             // cases like:
491b5840353SAdam Hornáček             // || path.endsWith("/..") || path.endsWith("/.")
492b5840353SAdam Hornáček             buf.setLength(buf.length() - 1);
493b5840353SAdam Hornáček         }
494b5840353SAdam Hornáček         return buf.toString();
495b5840353SAdam Hornáček     }
496b5840353SAdam Hornáček 
497d1e826faSAdam Hornáček     private static final Pattern EMAIL_PATTERN
498b5840353SAdam Hornáček             = Pattern.compile("([^<\\s]+@[^>\\s]+)");
499b5840353SAdam Hornáček 
500b5840353SAdam Hornáček     /**
501b5840353SAdam Hornáček      * Get email address of the author.
502b5840353SAdam Hornáček      *
503b5840353SAdam Hornáček      * @param author string containing author and possibly email address.
504b5840353SAdam Hornáček      * @return email address of the author or full author string if the author
505b5840353SAdam Hornáček      * string does not contain an email address.
506b5840353SAdam Hornáček      */
getEmail(String author)507b5840353SAdam Hornáček     public static String getEmail(String author) {
508b5840353SAdam Hornáček         Matcher emailMatcher = EMAIL_PATTERN.matcher(author);
509b5840353SAdam Hornáček         String email = author;
510b5840353SAdam Hornáček         if (emailMatcher.find()) {
511b5840353SAdam Hornáček             email = emailMatcher.group(1).trim();
512b5840353SAdam Hornáček         }
513b5840353SAdam Hornáček 
514b5840353SAdam Hornáček         return email;
515b5840353SAdam Hornáček     }
516b5840353SAdam Hornáček 
517b5840353SAdam Hornáček     /**
518b5840353SAdam Hornáček      * Remove all empty and {@code null} string elements from the given
519b5840353SAdam Hornáček      * <var>names</var> and optionally all redundant information like "." and
520b5840353SAdam Hornáček      * "..".
521b5840353SAdam Hornáček      *
522b5840353SAdam Hornáček      * @param names names to check
523b5840353SAdam Hornáček      * @param canonical if {@code true}, remove redundant elements as well.
524b5840353SAdam Hornáček      * @return a possible empty array of names all with a length &gt; 0.
525b5840353SAdam Hornáček      */
normalize(String[] names, boolean canonical)526b5840353SAdam Hornáček     private static String[] normalize(String[] names, boolean canonical) {
527b5840353SAdam Hornáček         LinkedList<String> res = new LinkedList<>();
528b5840353SAdam Hornáček         if (names == null || names.length == 0) {
529b5840353SAdam Hornáček             return new String[0];
530b5840353SAdam Hornáček         }
531b5840353SAdam Hornáček         for (String name : names) {
532b5840353SAdam Hornáček             if (name == null || name.length() == 0) {
533b5840353SAdam Hornáček                 continue;
534b5840353SAdam Hornáček             }
535b5840353SAdam Hornáček             if (canonical) {
536b5840353SAdam Hornáček                 if (name.equals("..")) {
537b5840353SAdam Hornáček                     if (!res.isEmpty()) {
538b5840353SAdam Hornáček                         res.removeLast();
539b5840353SAdam Hornáček                     }
540ff44f24aSAdam Hornáček                 } else if (!name.equals(".")) {
541b5840353SAdam Hornáček                     res.add(name);
542b5840353SAdam Hornáček                 }
543b5840353SAdam Hornáček             } else {
544b5840353SAdam Hornáček                 res.add(name);
545b5840353SAdam Hornáček             }
546b5840353SAdam Hornáček         }
547b13c5a0eSAdam Hornacek         return res.size() == names.length ? names : res.toArray(new String[0]);
548b5840353SAdam Hornáček     }
549b5840353SAdam Hornáček 
550b5840353SAdam Hornáček     /**
551b5840353SAdam Hornáček      * Generate a regexp that matches the specified character. Escape it in case
552b5840353SAdam Hornáček      * it is a character that has a special meaning in a regexp.
553b5840353SAdam Hornáček      *
554b5840353SAdam Hornáček      * @param c the character that the regexp should match
555b5840353SAdam Hornáček      * @return a six-character string in the form of <code>&#92;u</code><i>hhhh</i>
556b5840353SAdam Hornáček      */
escapeForRegex(char c)557b5840353SAdam Hornáček     private static String escapeForRegex(char c) {
558b5840353SAdam Hornáček         StringBuilder sb = new StringBuilder(6);
559b5840353SAdam Hornáček         sb.append("\\u");
560b5840353SAdam Hornáček         String hex = Integer.toHexString(c);
561b13c5a0eSAdam Hornacek         sb.append("0".repeat(4 - hex.length()));
562b5840353SAdam Hornáček         sb.append(hex);
563b5840353SAdam Hornáček         return sb.toString();
564b5840353SAdam Hornáček     }
565b5840353SAdam Hornáček 
566b13c5a0eSAdam Hornacek     private static final NumberFormat FORMATTER = new DecimalFormat("#,###,###,###.#");
567b5840353SAdam Hornáček 
568b13c5a0eSAdam Hornacek     private static final NumberFormat COUNT_FORMATTER = new DecimalFormat("#,###,###,###");
569b5840353SAdam Hornáček 
570b5840353SAdam Hornáček     /**
571b5840353SAdam Hornáček      * Convert the given size into a human readable string.
572b5840353SAdam Hornáček      *
573b5840353SAdam Hornáček      * NOTE: when changing the output of this function make sure to adapt the
574b5840353SAdam Hornáček      * jQuery tablesorter custom parsers in web/httpheader.jspf
575b5840353SAdam Hornáček      *
576b5840353SAdam Hornáček      * @param num size to convert.
577b5840353SAdam Hornáček      * @return a readable string
578b5840353SAdam Hornáček      */
readableSize(long num)579b5840353SAdam Hornáček     public static String readableSize(long num) {
580b5840353SAdam Hornáček         float l = num;
581b5840353SAdam Hornáček         NumberFormat formatter = (NumberFormat) FORMATTER.clone();
582b5840353SAdam Hornáček         if (l < 1024) {
583b5840353SAdam Hornáček             return formatter.format(l) + ' '; // for none-dirs append 'B'? ...
584b5840353SAdam Hornáček         } else if (l < 1048576) {
585b5840353SAdam Hornáček             return (formatter.format(l / 1024) + " KiB");
586b5840353SAdam Hornáček         } else if (l < 1073741824) {
587b5840353SAdam Hornáček             return ("<b>" + formatter.format(l / 1048576) + " MiB</b>");
588b5840353SAdam Hornáček         } else {
589b5840353SAdam Hornáček             return ("<b>" + formatter.format(l / 1073741824) + " GiB</b>");
590b5840353SAdam Hornáček         }
591b5840353SAdam Hornáček     }
592b5840353SAdam Hornáček 
593b5840353SAdam Hornáček     /**
594b5840353SAdam Hornáček      * Convert the specified {@code count} into a human readable string.
595b5840353SAdam Hornáček      * @param count value to convert.
596b5840353SAdam Hornáček      * @return a readable string
597b5840353SAdam Hornáček      */
readableCount(long count)598b5840353SAdam Hornáček     public static String readableCount(long count) {
5990402c7e4SChris Fraire         return readableCount(count, false);
6000402c7e4SChris Fraire     }
6010402c7e4SChris Fraire 
6020402c7e4SChris Fraire     /**
6030402c7e4SChris Fraire      * Convert the specified {@code count} into a human readable string.
6040402c7e4SChris Fraire      * @param isKnownDirectory a value indicating if {@code count} is known to
6050402c7e4SChris Fraire      *                         be for a directory
6060402c7e4SChris Fraire      * @param count value to convert.
6070402c7e4SChris Fraire      * @return a readable string
6080402c7e4SChris Fraire      */
readableCount(long count, boolean isKnownDirectory)6090402c7e4SChris Fraire     public static String readableCount(long count, boolean isKnownDirectory) {
610b5840353SAdam Hornáček         NumberFormat formatter = (NumberFormat) COUNT_FORMATTER.clone();
6110402c7e4SChris Fraire         if (isKnownDirectory || count < BOLD_COUNT_THRESHOLD) {
612b5840353SAdam Hornáček             return formatter.format(count);
613b5840353SAdam Hornáček         } else {
614b5840353SAdam Hornáček             return "<b>" + formatter.format(count) + "</b>";
615b5840353SAdam Hornáček         }
616b5840353SAdam Hornáček     }
617b5840353SAdam Hornáček 
618b5840353SAdam Hornáček     /**
619b5840353SAdam Hornáček      * Converts different HTML special characters into their encodings used in
620b5840353SAdam Hornáček      * html.
621b5840353SAdam Hornáček      *
622b5840353SAdam Hornáček      * @param s input text
623b5840353SAdam Hornáček      * @return encoded text for use in &lt;a title=""&gt; tag
624b5840353SAdam Hornáček      */
encode(String s)625b5840353SAdam Hornáček     public static String encode(String s) {
626b5840353SAdam Hornáček         /**
627b5840353SAdam Hornáček          * Make sure that the buffer is long enough to contain the whole string
628b5840353SAdam Hornáček          * with the expanded special characters. We use 1.5*length as a
629b5840353SAdam Hornáček          * heuristic.
630b5840353SAdam Hornáček          */
631b5840353SAdam Hornáček         StringBuilder sb = new StringBuilder((int) Math.max(10, s.length() * 1.5));
632b5840353SAdam Hornáček         try {
633b5840353SAdam Hornáček             encode(s, sb);
634b5840353SAdam Hornáček         } catch (IOException ex) {
635b5840353SAdam Hornáček             // IOException cannot happen when the destination is a
636b5840353SAdam Hornáček             // StringBuilder. Wrap in an AssertionError so that callers
637b5840353SAdam Hornáček             // don't have to check for an IOException that should never
638b5840353SAdam Hornáček             // happen.
639b5840353SAdam Hornáček             throw new AssertionError("StringBuilder threw IOException", ex);
640b5840353SAdam Hornáček         }
641b5840353SAdam Hornáček         return sb.toString();
642b5840353SAdam Hornáček     }
643b5840353SAdam Hornáček 
644b5840353SAdam Hornáček     /**
645b5840353SAdam Hornáček      * Converts different HTML special characters into their encodings used in
646b5840353SAdam Hornáček      * html.
647b5840353SAdam Hornáček      *
648b5840353SAdam Hornáček      * @param s input text
649b5840353SAdam Hornáček      * @param dest appendable destination for appending the encoded characters
65081b586e6SVladimir Kotal      * @throws java.io.IOException I/O exception
651b5840353SAdam Hornáček      */
encode(String s, Appendable dest)652b5840353SAdam Hornáček     public static void encode(String s, Appendable dest) throws IOException {
653b5840353SAdam Hornáček         for (int i = 0; i < s.length(); i++) {
654b5840353SAdam Hornáček             char c = s.charAt(i);
655b5840353SAdam Hornáček             if (c > 127 || c == '"' || c == '<' || c == '>' || c == '&' || c == '\'') {
656b5840353SAdam Hornáček                 // special html characters
657b5840353SAdam Hornáček                 dest.append("&#").append("" + (int) c).append(";");
658b5840353SAdam Hornáček             } else if (c == ' ') {
659b5840353SAdam Hornáček                 // non breaking space
660b5840353SAdam Hornáček                 dest.append("&nbsp;");
661b5840353SAdam Hornáček             } else if (c == '\t') {
662b5840353SAdam Hornáček                 dest.append("&nbsp;&nbsp;&nbsp;&nbsp;");
663b5840353SAdam Hornáček             } else if (c == '\n') {
664b5840353SAdam Hornáček                 // <br/>
665b5840353SAdam Hornáček                 dest.append("&lt;br/&gt;");
666b5840353SAdam Hornáček             } else {
667b5840353SAdam Hornáček                 dest.append(c);
668b5840353SAdam Hornáček             }
669b5840353SAdam Hornáček         }
670b5840353SAdam Hornáček     }
671b5840353SAdam Hornáček 
672b5840353SAdam Hornáček     /**
673ff44f24aSAdam Hornáček      * Encode URL.
674b5840353SAdam Hornáček      *
675b5840353SAdam Hornáček      * @param urlStr string URL
676b5840353SAdam Hornáček      * @return the encoded URL
67781b586e6SVladimir Kotal      * @throws URISyntaxException URI syntax
67881b586e6SVladimir Kotal      * @throws MalformedURLException URL malformed
679b5840353SAdam Hornáček      */
encodeURL(String urlStr)680b5840353SAdam Hornáček     public static String encodeURL(String urlStr) throws URISyntaxException, MalformedURLException {
681b5840353SAdam Hornáček         URL url = new URL(urlStr);
682b5840353SAdam Hornáček         URI constructed = new URI(url.getProtocol(), url.getUserInfo(),
683b5840353SAdam Hornáček                 url.getHost(), url.getPort(),
684b5840353SAdam Hornáček                 url.getPath(), url.getQuery(), url.getRef());
685b5840353SAdam Hornáček         return constructed.toString();
686b5840353SAdam Hornáček     }
687b5840353SAdam Hornáček 
688b5840353SAdam Hornáček     /**
689b5840353SAdam Hornáček      * Write out line information wrt. to the given annotation in the format:
690b5840353SAdam Hornáček      * {@code Linenumber Blame Author} incl. appropriate links.
691b5840353SAdam Hornáček      *
692b5840353SAdam Hornáček      * @param num linenumber to print
693b5840353SAdam Hornáček      * @param out print destination
694b5840353SAdam Hornáček      * @param annotation annotation to use. If {@code null} only the linenumber
695b5840353SAdam Hornáček      * gets printed.
696b5840353SAdam Hornáček      * @param userPageLink see {@link RuntimeEnvironment#getUserPage()}
697b5840353SAdam Hornáček      * @param userPageSuffix see {@link RuntimeEnvironment#getUserPageSuffix()}
698b5840353SAdam Hornáček      * @param project project that is used
699b5840353SAdam Hornáček      * @throws IOException depends on the destination (<var>out</var>).
700b5840353SAdam Hornáček      */
readableLine(int num, Writer out, Annotation annotation, String userPageLink, String userPageSuffix, String project)701b5840353SAdam Hornáček     public static void readableLine(int num, Writer out, Annotation annotation,
702b5840353SAdam Hornáček             String userPageLink, String userPageSuffix, String project)
703ff44f24aSAdam Hornáček             throws IOException {
704b5840353SAdam Hornáček         readableLine(num, out, annotation, userPageLink, userPageSuffix, project, false);
705b5840353SAdam Hornáček     }
706b5840353SAdam Hornáček 
readableLine(int num, Writer out, Annotation annotation, String userPageLink, String userPageSuffix, String project, boolean skipNewline)707ff44f24aSAdam Hornáček     public static void readableLine(int num, Writer out, Annotation annotation, String userPageLink,
708ff44f24aSAdam Hornáček             String userPageSuffix, String project, boolean skipNewline)
709ff44f24aSAdam Hornáček             throws IOException {
710b5840353SAdam Hornáček         // this method should go to JFlexXref
711b5840353SAdam Hornáček         String snum = String.valueOf(num);
712b5840353SAdam Hornáček         if (num > 1 && !skipNewline) {
713b5840353SAdam Hornáček             out.write("\n");
714b5840353SAdam Hornáček         }
715b5840353SAdam Hornáček         out.write(anchorClassStart);
716b5840353SAdam Hornáček         out.write(num % 10 == 0 ? "hl" : "l");
717b5840353SAdam Hornáček         out.write("\" name=\"");
718b5840353SAdam Hornáček         out.write(snum);
719b5840353SAdam Hornáček         out.write("\" href=\"#");
720b5840353SAdam Hornáček         out.write(snum);
721b5840353SAdam Hornáček         out.write(closeQuotedTag);
722b5840353SAdam Hornáček         out.write(snum);
723b5840353SAdam Hornáček         out.write(anchorEnd);
724754a7a39SVladimir Kotal 
725b5840353SAdam Hornáček         if (annotation != null) {
726754a7a39SVladimir Kotal             writeAnnotation(num, out, annotation, userPageLink, userPageSuffix, project);
727754a7a39SVladimir Kotal         }
728754a7a39SVladimir Kotal     }
729754a7a39SVladimir Kotal 
writeAnnotation(int num, Writer out, Annotation annotation, String userPageLink, String userPageSuffix, String project)730754a7a39SVladimir Kotal     private static void writeAnnotation(int num, Writer out, Annotation annotation, String userPageLink,
731754a7a39SVladimir Kotal                                         String userPageSuffix, String project) throws IOException {
732b5840353SAdam Hornáček         String r = annotation.getRevision(num);
733b5840353SAdam Hornáček         boolean enabled = annotation.isEnabled(num);
734b5840353SAdam Hornáček         out.write("<span class=\"blame\">");
735b5840353SAdam Hornáček         if (enabled) {
736b5840353SAdam Hornáček             out.write(anchorClassStart);
737b5840353SAdam Hornáček             out.write("r");
738e87b8f8cSKryštof Tulinger             out.write("\" style=\"background-color: ");
739e87b8f8cSKryštof Tulinger             out.write(annotation.getColors().getOrDefault(r, "inherit"));
740b5840353SAdam Hornáček             out.write("\" href=\"");
741*d6df19e1SAdam Hornacek             out.write(uriEncode(annotation.getFilename()));
7421c830032SChris Fraire             out.write("?");
7431c830032SChris Fraire             out.write(QueryParameters.ANNOTATION_PARAM_EQ_TRUE);
7441c830032SChris Fraire             out.write("&amp;");
7451c830032SChris Fraire             out.write(QueryParameters.REVISION_PARAM_EQ);
746*d6df19e1SAdam Hornacek             out.write(uriEncode(r));
747b5840353SAdam Hornáček             String msg = annotation.getDesc(r);
748b5840353SAdam Hornáček             out.write("\" title=\"");
749b5840353SAdam Hornáček             if (msg != null) {
750754a7a39SVladimir Kotal                 out.write(Util.encode(msg));
751b5840353SAdam Hornáček             }
752b5840353SAdam Hornáček             if (annotation.getFileVersion(r) != 0) {
753b5840353SAdam Hornáček                 out.write("&lt;br/&gt;version: " + annotation.getFileVersion(r) + "/"
754b5840353SAdam Hornáček                         + annotation.getRevisions().size());
755b5840353SAdam Hornáček             }
756b5840353SAdam Hornáček             out.write(closeQuotedTag);
757b5840353SAdam Hornáček         }
758b5840353SAdam Hornáček         StringBuilder buf = new StringBuilder();
759e87b8f8cSKryštof Tulinger         final boolean most_recent_revision = annotation.getFileVersion(r) == annotation.getRevisions().size();
760e87b8f8cSKryštof Tulinger         // print an asterisk for the most recent revision
761e87b8f8cSKryštof Tulinger         if (most_recent_revision) {
762e87b8f8cSKryštof Tulinger             buf.append("<span class=\"most_recent_revision\">");
763e87b8f8cSKryštof Tulinger             buf.append('*');
764e87b8f8cSKryštof Tulinger         }
765b5840353SAdam Hornáček         htmlize(r, buf);
766e87b8f8cSKryštof Tulinger         if (most_recent_revision) {
767e87b8f8cSKryštof Tulinger             buf.append("</span>"); // recent revision span
768e87b8f8cSKryštof Tulinger         }
769b5840353SAdam Hornáček         out.write(buf.toString());
770b5840353SAdam Hornáček         buf.setLength(0);
771b5840353SAdam Hornáček         if (enabled) {
772b5840353SAdam Hornáček             RuntimeEnvironment env = RuntimeEnvironment.getInstance();
773b5840353SAdam Hornáček 
774b5840353SAdam Hornáček             out.write(anchorEnd);
775b5840353SAdam Hornáček 
776b5840353SAdam Hornáček             // Write link to search the revision in current project.
777b5840353SAdam Hornáček             out.write(anchorClassStart);
778b5840353SAdam Hornáček             out.write("search\" href=\"" + env.getUrlPrefix());
7791c830032SChris Fraire             out.write(QueryParameters.DEFS_SEARCH_PARAM_EQ);
7801c830032SChris Fraire             out.write("&amp;");
7811c830032SChris Fraire             out.write(QueryParameters.REFS_SEARCH_PARAM_EQ);
7821c830032SChris Fraire             out.write("&amp;");
7831c830032SChris Fraire             out.write(QueryParameters.PATH_SEARCH_PARAM_EQ);
784b5840353SAdam Hornáček             out.write(project);
7851c830032SChris Fraire             out.write("&amp;");
7861c830032SChris Fraire             out.write(QueryParameters.HIST_SEARCH_PARAM_EQ);
7871c830032SChris Fraire             out.write("&quot;");
788*d6df19e1SAdam Hornacek             out.write(uriEncode(r));
7891c830032SChris Fraire             out.write("&quot;&amp;");
7901c830032SChris Fraire             out.write(QueryParameters.TYPE_SEARCH_PARAM_EQ);
7911c830032SChris Fraire             out.write("\" title=\"Search history for this revision");
792b5840353SAdam Hornáček             out.write(closeQuotedTag);
793b5840353SAdam Hornáček             out.write("S");
794b5840353SAdam Hornáček             out.write(anchorEnd);
795b5840353SAdam Hornáček         }
796b5840353SAdam Hornáček         String a = annotation.getAuthor(num);
797b5840353SAdam Hornáček         if (userPageLink == null) {
798b5840353SAdam Hornáček             out.write(HtmlConsts.SPAN_A);
799b5840353SAdam Hornáček             htmlize(a, buf);
800b5840353SAdam Hornáček             out.write(buf.toString());
801b5840353SAdam Hornáček             out.write(HtmlConsts.ZSPAN);
802b5840353SAdam Hornáček             buf.setLength(0);
803b5840353SAdam Hornáček         } else {
804b5840353SAdam Hornáček             out.write(anchorClassStart);
805b5840353SAdam Hornáček             out.write("a\" href=\"");
806b5840353SAdam Hornáček             out.write(userPageLink);
807*d6df19e1SAdam Hornacek             out.write(uriEncode(a));
808b5840353SAdam Hornáček             if (userPageSuffix != null) {
809b5840353SAdam Hornáček                 out.write(userPageSuffix);
810b5840353SAdam Hornáček             }
811b5840353SAdam Hornáček             out.write(closeQuotedTag);
812b5840353SAdam Hornáček             htmlize(a, buf);
813b5840353SAdam Hornáček             out.write(buf.toString());
814b5840353SAdam Hornáček             buf.setLength(0);
815b5840353SAdam Hornáček             out.write(anchorEnd);
816b5840353SAdam Hornáček         }
817b5840353SAdam Hornáček         out.write("</span>");
818b5840353SAdam Hornáček     }
819b5840353SAdam Hornáček 
820b5840353SAdam Hornáček     /**
821b5840353SAdam Hornáček      * Generate a string from the given path and date in a way that allows
822b5840353SAdam Hornáček      * stable lexicographic sorting (i.e. gives always the same results) as a
823b5840353SAdam Hornáček      * walk of the file hierarchy. Thus null character (\u0000) is used both to
824b5840353SAdam Hornáček      * separate directory components and to separate the path from the date.
825b5840353SAdam Hornáček      *
826b5840353SAdam Hornáček      * @param path path to mangle.
827b5840353SAdam Hornáček      * @param date date string to use.
828b5840353SAdam Hornáček      * @return the mangled path.
829b5840353SAdam Hornáček      */
path2uid(String path, String date)830b5840353SAdam Hornáček     public static String path2uid(String path, String date) {
831b5840353SAdam Hornáček         return path.replace('/', '\u0000') + "\u0000" + date;
832b5840353SAdam Hornáček     }
833b5840353SAdam Hornáček 
834b5840353SAdam Hornáček     /**
835b5840353SAdam Hornáček      * The reverse operation for {@link #path2uid(String, String)} - re-creates
836b5840353SAdam Hornáček      * the unmangled path from the given uid.
837b5840353SAdam Hornáček      *
838b5840353SAdam Hornáček      * @param uid uid to unmangle.
839b5840353SAdam Hornáček      * @return the original path.
840b5840353SAdam Hornáček      */
uid2url(String uid)841b5840353SAdam Hornáček     public static String uid2url(String uid) {
842807ead8fSLubos Kosco         String url = uid.replace('\u0000', PATH_SEPARATOR);
843807ead8fSLubos Kosco         return url.substring(0, url.lastIndexOf(PATH_SEPARATOR)); // remove date from end
844807ead8fSLubos Kosco     }
845807ead8fSLubos Kosco 
8460241b5b5SChris Fraire     /**
8476c62ede9SAdam Hornacek      * Sanitizes Windows path delimiters (if {@link SystemUtils#IS_OS_WINDOWS}
8480241b5b5SChris Fraire      * is {@code true}) as
8490241b5b5SChris Fraire      * {@link org.opengrok.indexer.index.Indexer#PATH_SEPARATOR} in order not
8500241b5b5SChris Fraire      * to conflict with the Lucene escape character and also so {@code path}
8510241b5b5SChris Fraire      * appears as a correctly formed URI in search results.
8520241b5b5SChris Fraire      */
fixPathIfWindows(String path)853807ead8fSLubos Kosco     public static String fixPathIfWindows(String path) {
8546c62ede9SAdam Hornacek         if (path != null && SystemUtils.IS_OS_WINDOWS) {
855807ead8fSLubos Kosco             return path.replace(File.separatorChar, PATH_SEPARATOR);
856807ead8fSLubos Kosco         }
857807ead8fSLubos Kosco         return path;
858807ead8fSLubos Kosco     }
859807ead8fSLubos Kosco 
860cf51835aSKryštof Tulinger     /**
861b5840353SAdam Hornáček      * Write the 'H A D' links. This is used for search results and directory
862b5840353SAdam Hornáček      * listings.
863b5840353SAdam Hornáček      *
864b5840353SAdam Hornáček      * @param out writer for producing output
865b5840353SAdam Hornáček      * @param ctxE URI encoded prefix
866b5840353SAdam Hornáček      * @param entry file/directory name to write
8670ec550ccSAdam Hornacek      * @param isDir is directory
868b5840353SAdam Hornáček      * @throws IOException depends on the destination (<var>out</var>).
869b5840353SAdam Hornáček      */
writeHAD(Writer out, String ctxE, String entry, boolean isDir)8700ec550ccSAdam Hornacek     public static void writeHAD(Writer out, String ctxE, String entry, boolean isDir) throws IOException {
871b5840353SAdam Hornáček 
872b5840353SAdam Hornáček         String downloadPrefixE = ctxE + Prefix.DOWNLOAD_P;
873b5840353SAdam Hornáček         String xrefPrefixE = ctxE + Prefix.XREF_P;
874b5840353SAdam Hornáček 
875b5840353SAdam Hornáček         out.write("<td class=\"q\">");
876b5840353SAdam Hornáček         if (RuntimeEnvironment.getInstance().isHistoryEnabled()) {
877b5840353SAdam Hornáček             String histPrefixE = ctxE + Prefix.HIST_L;
878b5840353SAdam Hornáček 
879b5840353SAdam Hornáček             out.write("<a href=\"");
880b5840353SAdam Hornáček             out.write(histPrefixE);
881b5840353SAdam Hornáček             if (!entry.startsWith("/")) {
882b5840353SAdam Hornáček                 entry = "/" + entry;
883b5840353SAdam Hornáček             }
884b5840353SAdam Hornáček             out.write(entry);
885b5840353SAdam Hornáček             out.write("\" title=\"History\">H</a>");
886b5840353SAdam Hornáček         }
887b5840353SAdam Hornáček 
8880ec550ccSAdam Hornacek         if (!isDir) {
889b5840353SAdam Hornáček             out.write(" <a href=\"");
890b5840353SAdam Hornáček             out.write(xrefPrefixE);
891b5840353SAdam Hornáček             out.write(entry);
8921c830032SChris Fraire             out.write("?");
8931c830032SChris Fraire             out.write(QueryParameters.ANNOTATION_PARAM_EQ_TRUE);
8941c830032SChris Fraire             out.write("\" title=\"Annotate\">A</a> ");
895b5840353SAdam Hornáček             out.write("<a href=\"");
896b5840353SAdam Hornáček             out.write(downloadPrefixE);
897b5840353SAdam Hornáček             out.write(entry);
898b5840353SAdam Hornáček             out.write("\" title=\"Download\">D</a>");
899b5840353SAdam Hornáček         }
900b5840353SAdam Hornáček 
901b5840353SAdam Hornáček         out.write("</td>");
902b5840353SAdam Hornáček     }
903b5840353SAdam Hornáček 
904b5840353SAdam Hornáček     /**
905ff44f24aSAdam Hornáček      * Wrapper around UTF-8 URL encoding of a string.
906b5840353SAdam Hornáček      *
907379f387bSVladimir Kotal      * @param q query to be encoded. If {@code null}, an empty string will be used instead.
908379f387bSVladimir Kotal      * @return null if failed, otherwise the encoded string
909b5840353SAdam Hornáček      * @see URLEncoder#encode(String, String)
910b5840353SAdam Hornáček      */
uriEncode(String q)911*d6df19e1SAdam Hornacek     public static String uriEncode(String q) {
912f22e1845SAdam Hornacek         return q == null ? "" : URLEncoder.encode(q, StandardCharsets.UTF_8);
913b5840353SAdam Hornáček     }
914b5840353SAdam Hornáček 
915b5840353SAdam Hornáček     /**
916b5840353SAdam Hornáček      * Append to {@code dest} the UTF-8 URL-encoded representation of
917b5840353SAdam Hornáček      * {@code str}.
918b5840353SAdam Hornáček      * @param str a defined instance
919b5840353SAdam Hornáček      * @param dest a defined target
92081b586e6SVladimir Kotal      * @throws IOException I/O
921b5840353SAdam Hornáček      */
uriEncode(String str, Appendable dest)922*d6df19e1SAdam Hornacek     public static void uriEncode(String str, Appendable dest)
923b5840353SAdam Hornáček             throws IOException {
924*d6df19e1SAdam Hornacek         String uenc = uriEncode(str);
925b5840353SAdam Hornáček         dest.append(uenc);
926b5840353SAdam Hornáček     }
927b5840353SAdam Hornáček 
928b5840353SAdam Hornáček     /**
929b5840353SAdam Hornáček      * Append '&amp;name=value" to the given buffer. If the given
930b5840353SAdam Hornáček      * <var>value</var>
931b5840353SAdam Hornáček      * is {@code null}, this method does nothing.
932b5840353SAdam Hornáček      *
933b5840353SAdam Hornáček      * @param buf where to append the query string
934b5840353SAdam Hornáček      * @param key the name of the parameter to add. Append as is!
935b5840353SAdam Hornáček      * @param value the value for the given parameter. Gets automatically UTF-8
936b5840353SAdam Hornáček      * URL encoded.
937b5840353SAdam Hornáček      * @throws NullPointerException if the given buffer is {@code null}.
938*d6df19e1SAdam Hornacek      * @see #uriEncode(String)
939b5840353SAdam Hornáček      */
appendQuery(StringBuilder buf, String key, String value)940b5840353SAdam Hornáček     public static void appendQuery(StringBuilder buf, String key,
941b5840353SAdam Hornáček             String value) {
942b5840353SAdam Hornáček 
943b5840353SAdam Hornáček         if (value != null) {
944*d6df19e1SAdam Hornacek             buf.append("&amp;").append(key).append('=').append(uriEncode(value));
945b5840353SAdam Hornáček         }
946b5840353SAdam Hornáček     }
947b5840353SAdam Hornáček 
948b5840353SAdam Hornáček     /**
949b5840353SAdam Hornáček      * URI encode the given path.
950b5840353SAdam Hornáček      *
951b5840353SAdam Hornáček      * @param path path to encode.
952b5840353SAdam Hornáček      * @return the encoded path.
953b5840353SAdam Hornáček      * @throws NullPointerException if a parameter is {@code null}
954b5840353SAdam Hornáček      */
uriEncodePath(String path)955*d6df19e1SAdam Hornacek     public static String uriEncodePath(String path) {
956b5840353SAdam Hornáček         // Bug #19188: Ideally, we'd like to use the standard class library to
957b5840353SAdam Hornáček         // encode the paths. We're aware of the following two methods:
958b5840353SAdam Hornáček         //
959b5840353SAdam Hornáček         // 1) URLEncoder.encode() - this method however transforms space to +
960b5840353SAdam Hornáček         // instead of %20 (which is right for HTML form data, but not for
961b5840353SAdam Hornáček         // paths), and it also does not preserve the separator chars (/).
962b5840353SAdam Hornáček         //
963b5840353SAdam Hornáček         // 2) URI.getRawPath() - transforms space and / as expected, but gets
964b5840353SAdam Hornáček         // confused when the path name contains a colon character (it thinks
965b5840353SAdam Hornáček         // parts of the path is schema in that case)
966b5840353SAdam Hornáček         //
967b5840353SAdam Hornáček         // For now, encode manually the way we want it.
968b5840353SAdam Hornáček         StringBuilder sb = new StringBuilder(path.length());
969b5840353SAdam Hornáček         for (byte b : path.getBytes(StandardCharsets.UTF_8)) {
970b5840353SAdam Hornáček             // URLEncoder's javadoc says a-z, A-Z, ., -, _ and * are safe
971b5840353SAdam Hornáček             // characters, so we preserve those to make the encoded string
972b5840353SAdam Hornáček             // shorter and easier to read. We also preserve the separator
973b5840353SAdam Hornáček             // chars (/). All other characters are encoded (as UTF-8 byte
974b5840353SAdam Hornáček             // sequences).
975b5840353SAdam Hornáček             if ((b == '/') || (b >= 'a' && b <= 'z')
976b5840353SAdam Hornáček                     || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9')
977b5840353SAdam Hornáček                     || (b == '.') || (b == '-') || (b == '_') || (b == '*')) {
978b5840353SAdam Hornáček                 sb.append((char) b);
979b5840353SAdam Hornáček             } else {
980b5840353SAdam Hornáček                 sb.append('%');
981b5840353SAdam Hornáček                 int u = b & 0xFF;  // Make the byte value unsigned.
982b5840353SAdam Hornáček                 if (u <= 0x0F) {
983b5840353SAdam Hornáček                     // Add leading zero if required.
984b5840353SAdam Hornáček                     sb.append('0');
985b5840353SAdam Hornáček                 }
98652dccac1SChris Fraire                 sb.append(Integer.toHexString(u).toUpperCase(Locale.ROOT));
987b5840353SAdam Hornáček             }
988b5840353SAdam Hornáček         }
989b5840353SAdam Hornáček         return sb.toString();
990b5840353SAdam Hornáček     }
991b5840353SAdam Hornáček 
992b5840353SAdam Hornáček     /**
993b5840353SAdam Hornáček      * Escape a string for use as in an HTML attribute value. The returned value
994b5840353SAdam Hornáček      * is not enclosed in double quotes. The caller needs to add those.
995b5840353SAdam Hornáček      *
996b5840353SAdam Hornáček      * @param q string to escape.
997b5840353SAdam Hornáček      * @return an empty string if a parameter is {@code null}, the mangled
998b5840353SAdam Hornáček      * string otherwise.
999b5840353SAdam Hornáček      */
formQuoteEscape(String q)1000b5840353SAdam Hornáček     public static String formQuoteEscape(String q) {
1001b5840353SAdam Hornáček         if (q == null || q.isEmpty()) {
1002b5840353SAdam Hornáček             return "";
1003b5840353SAdam Hornáček         }
1004b5840353SAdam Hornáček         StringBuilder sb = new StringBuilder();
1005b5840353SAdam Hornáček         char c;
1006b5840353SAdam Hornáček         for (int i = 0; i < q.length(); i++) {
1007b5840353SAdam Hornáček             c = q.charAt(i);
1008b5840353SAdam Hornáček             switch (c) {
1009b5840353SAdam Hornáček                 case '"':
1010b5840353SAdam Hornáček                     sb.append("&quot;");
1011b5840353SAdam Hornáček                     break;
1012b5840353SAdam Hornáček                 case '&':
1013b5840353SAdam Hornáček                     sb.append("&amp;");
1014b5840353SAdam Hornáček                     break;
1015b5840353SAdam Hornáček                 default:
1016b5840353SAdam Hornáček                     sb.append(c);
1017b5840353SAdam Hornáček                     break;
1018b5840353SAdam Hornáček             }
1019b5840353SAdam Hornáček         }
1020b5840353SAdam Hornáček         return sb.toString();
1021b5840353SAdam Hornáček     }
1022b5840353SAdam Hornáček 
1023b5840353SAdam Hornáček     /**
1024b5840353SAdam Hornáček      * Tag changes in the given <var>line1</var> and <var>line2</var>
1025b5840353SAdam Hornáček      * for highlighting. Removed parts are tagged with CSS class {@code d}, new
1026b5840353SAdam Hornáček      * parts are tagged with CSS class {@code a} using a {@code span} element.
1027b5840353SAdam Hornáček      * The input parameters must not have any HTML escapes in them.
1028b5840353SAdam Hornáček      *
1029b5840353SAdam Hornáček      * @param line1 line of the original file
1030b5840353SAdam Hornáček      * @param line2 line of the changed/new file
1031b5840353SAdam Hornáček      * @return the tagged lines (field[0] ~= line1, field[1] ~= line2).
1032b5840353SAdam Hornáček      * @throws NullPointerException if one of the given parameters is
1033b5840353SAdam Hornáček      * {@code null}.
1034b5840353SAdam Hornáček      */
diffline(StringBuilder line1, StringBuilder line2)1035b5840353SAdam Hornáček     public static String[] diffline(StringBuilder line1, StringBuilder line2) {
1036b5840353SAdam Hornáček         String[] ret = new String[2];
1037b5840353SAdam Hornáček         int s = 0;
1038b5840353SAdam Hornáček         int m = line1.length() - 1;
1039b5840353SAdam Hornáček         int n = line2.length() - 1;
1040b5840353SAdam Hornáček         while (s <= m && s <= n && (line1.charAt(s) == line2.charAt(s))) {
1041b5840353SAdam Hornáček             s++;
1042b5840353SAdam Hornáček         }
1043b5840353SAdam Hornáček 
1044b5840353SAdam Hornáček         while (s <= m && s <= n && (line1.charAt(m) == line2.charAt(n))) {
1045b5840353SAdam Hornáček             m--;
1046b5840353SAdam Hornáček             n--;
1047b5840353SAdam Hornáček         }
1048b5840353SAdam Hornáček 
1049b5840353SAdam Hornáček         // deleted
1050b5840353SAdam Hornáček         if (s <= m) {
1051b13c5a0eSAdam Hornacek             String sb = Util.htmlize(line1.substring(0, s)) +
1052b13c5a0eSAdam Hornacek                     HtmlConsts.SPAN_D +
1053b13c5a0eSAdam Hornacek                     Util.htmlize(line1.substring(s, m + 1)) +
1054b13c5a0eSAdam Hornacek                     HtmlConsts.ZSPAN +
1055b13c5a0eSAdam Hornacek                     Util.htmlize(line1.substring(m + 1, line1.length()));
1056b13c5a0eSAdam Hornacek             ret[0] = sb;
1057b5840353SAdam Hornáček         } else {
1058b5840353SAdam Hornáček             ret[0] = Util.htmlize(line1.toString()); // no change
1059b5840353SAdam Hornáček         }
1060b5840353SAdam Hornáček 
1061b5840353SAdam Hornáček         // added
1062b5840353SAdam Hornáček         if (s <= n) {
1063b13c5a0eSAdam Hornacek             String sb = Util.htmlize(line2.substring(0, s)) +
1064b13c5a0eSAdam Hornacek                     HtmlConsts.SPAN_A +
1065b13c5a0eSAdam Hornacek                     Util.htmlize(line2.substring(s, n + 1)) +
1066b13c5a0eSAdam Hornacek                     HtmlConsts.ZSPAN +
1067b13c5a0eSAdam Hornacek                     Util.htmlize(line2.substring(n + 1, line2.length()));
1068b13c5a0eSAdam Hornacek             ret[1] = sb;
1069b5840353SAdam Hornáček         } else {
1070b5840353SAdam Hornáček             ret[1] = Util.htmlize(line2.toString()); // no change
1071b5840353SAdam Hornáček         }
1072b5840353SAdam Hornáček 
1073b5840353SAdam Hornáček         return ret;
1074b5840353SAdam Hornáček     }
1075b5840353SAdam Hornáček 
1076b5840353SAdam Hornáček     /**
1077b5840353SAdam Hornáček      * Dump the configuration as an HTML table.
1078b5840353SAdam Hornáček      *
1079b5840353SAdam Hornáček      * @param out destination for the HTML output
1080b5840353SAdam Hornáček      * @throws IOException if an error happens while writing to {@code out}
1081b5840353SAdam Hornáček      * @throws HistoryException if the history guru cannot be accesses
1082b5840353SAdam Hornáček      */
1083b5840353SAdam Hornáček     @SuppressWarnings("boxing")
dumpConfiguration(Appendable out)1084b5840353SAdam Hornáček     public static void dumpConfiguration(Appendable out) throws IOException,
1085b5840353SAdam Hornáček             HistoryException {
1086b5840353SAdam Hornáček         out.append("<table border=\"1\" width=\"100%\">");
1087b5840353SAdam Hornáček         out.append("<tr><th>Variable</th><th>Value</th></tr>");
1088b5840353SAdam Hornáček         RuntimeEnvironment env = RuntimeEnvironment.getInstance();
1089b5840353SAdam Hornáček         printTableRow(out, "Source root", env.getSourceRootPath());
1090b5840353SAdam Hornáček         printTableRow(out, "Data root", env.getDataRootPath());
1091b5840353SAdam Hornáček         printTableRow(out, "CTags", env.getCtags());
1092b5840353SAdam Hornáček         printTableRow(out, "Bug page", env.getBugPage());
1093b5840353SAdam Hornáček         printTableRow(out, "Bug pattern", env.getBugPattern());
1094b5840353SAdam Hornáček         printTableRow(out, "User page", env.getUserPage());
1095b5840353SAdam Hornáček         printTableRow(out, "User page suffix", env.getUserPageSuffix());
1096b5840353SAdam Hornáček         printTableRow(out, "Review page", env.getReviewPage());
1097b5840353SAdam Hornáček         printTableRow(out, "Review pattern", env.getReviewPattern());
1098b5840353SAdam Hornáček         printTableRow(out, "Using projects", env.isProjectsEnabled());
1099b5840353SAdam Hornáček         out.append("<tr><td>Ignored files</td><td>");
1100b5840353SAdam Hornáček         printUnorderedList(out, env.getIgnoredNames().getItems());
1101b5840353SAdam Hornáček         out.append("</td></tr>");
1102b5840353SAdam Hornáček         printTableRow(out, "lucene RAM_BUFFER_SIZE_MB", env.getRamBufferSize());
1103b5840353SAdam Hornáček         printTableRow(out, "Allow leading wildcard in search",
1104b5840353SAdam Hornáček                 env.isAllowLeadingWildcard());
1105b5840353SAdam Hornáček         printTableRow(out, "History cache", HistoryGuru.getInstance()
1106b5840353SAdam Hornáček                 .getCacheInfo());
1107b5840353SAdam Hornáček         printTableRow(out, "Authorization plugin directory", env.getPluginDirectory());
1108b5840353SAdam Hornáček         printTableRow(out, "Authorization watchdog directory", env.getPluginDirectory());
1109b5840353SAdam Hornáček         printTableRow(out, "Authorization watchdog enabled", env.isAuthorizationWatchdog());
1110b5840353SAdam Hornáček         printTableRow(out, "Authorization stack", "<pre>" + env.getAuthorizationFramework().getStack().hierarchyToString() + "</pre>");
1111b5840353SAdam Hornáček         out.append("</table>");
1112b5840353SAdam Hornáček     }
1113b5840353SAdam Hornáček 
1114b5840353SAdam Hornáček     /**
1115b5840353SAdam Hornáček      * Just read the given source and dump as is to the given destination. Does
1116b5840353SAdam Hornáček      * nothing, if one or more of the parameters is {@code null}.
1117b5840353SAdam Hornáček      *
1118b5840353SAdam Hornáček      * @param out write destination
1119b5840353SAdam Hornáček      * @param in source to read
1120b5840353SAdam Hornáček      * @throws IOException as defined by the given reader/writer
1121b5840353SAdam Hornáček      * @throws NullPointerException if a parameter is {@code null}.
1122b5840353SAdam Hornáček      */
dump(Writer out, Reader in)1123b5840353SAdam Hornáček     public static void dump(Writer out, Reader in) throws IOException {
1124b5840353SAdam Hornáček         if (in == null || out == null) {
1125b5840353SAdam Hornáček             return;
1126b5840353SAdam Hornáček         }
1127b5840353SAdam Hornáček         char[] buf = new char[8192];
1128b5840353SAdam Hornáček         int len = 0;
1129b5840353SAdam Hornáček         while ((len = in.read(buf)) >= 0) {
1130b5840353SAdam Hornáček             out.write(buf, 0, len);
1131b5840353SAdam Hornáček         }
1132b5840353SAdam Hornáček     }
1133b5840353SAdam Hornáček 
1134b5840353SAdam Hornáček     /**
1135b5840353SAdam Hornáček      * Silently dump a file to the given destination. All {@link IOException}s
1136b5840353SAdam Hornáček      * gets caught and logged, but not re-thrown.
1137b5840353SAdam Hornáček      *
1138b5840353SAdam Hornáček      * @param out dump destination
1139b5840353SAdam Hornáček      * @param dir directory, which should contains the file.
1140b5840353SAdam Hornáček      * @param filename the basename of the file to dump.
1141b5840353SAdam Hornáček      * @param compressed if {@code true} the denoted file is assumed to be
1142b5840353SAdam Hornáček      * gzipped.
1143b5840353SAdam Hornáček      * @return {@code true} on success (everything read and written).
1144b5840353SAdam Hornáček      * @throws NullPointerException if a parameter is {@code null}.
1145b5840353SAdam Hornáček      */
dump(Writer out, File dir, String filename, boolean compressed)1146b5840353SAdam Hornáček     public static boolean dump(Writer out, File dir, String filename,
1147b5840353SAdam Hornáček             boolean compressed) {
1148b5840353SAdam Hornáček         return dump(out, new File(dir, filename), compressed);
1149b5840353SAdam Hornáček     }
1150b5840353SAdam Hornáček 
1151b5840353SAdam Hornáček     /**
1152b5840353SAdam Hornáček      * Silently dump a file to the given destination. All {@link IOException}s
1153b5840353SAdam Hornáček      * gets caught and logged, but not re-thrown.
1154b5840353SAdam Hornáček      *
1155b5840353SAdam Hornáček      * @param out dump destination
1156b5840353SAdam Hornáček      * @param file file to dump.
1157b5840353SAdam Hornáček      * @param compressed if {@code true} the denoted file is assumed to be
1158b5840353SAdam Hornáček      * gzipped.
1159b5840353SAdam Hornáček      * @return {@code true} on success (everything read and written).
1160b5840353SAdam Hornáček      * @throws NullPointerException if a parameter is {@code null}.
1161b5840353SAdam Hornáček      */
dump(Writer out, File file, boolean compressed)1162b5840353SAdam Hornáček     public static boolean dump(Writer out, File file, boolean compressed) {
1163b5840353SAdam Hornáček         if (!file.exists()) {
1164b5840353SAdam Hornáček             return false;
1165b5840353SAdam Hornáček         }
1166b5840353SAdam Hornáček         /**
1167b5840353SAdam Hornáček          * For backward compatibility, read the OpenGrok-produced document
1168b5840353SAdam Hornáček          * using the system default charset.
1169b5840353SAdam Hornáček          */
11704b613dedSAdam Hornacek         try (InputStream iss = new BufferedInputStream(new FileInputStream(file))) {
11714b613dedSAdam Hornacek             try (Reader in = compressed ? new InputStreamReader(new GZIPInputStream(iss)) : new InputStreamReader(iss)) {
1172b5840353SAdam Hornáček                 dump(out, in);
11734b613dedSAdam Hornacek             }
1174b5840353SAdam Hornáček             return true;
1175b5840353SAdam Hornáček         } catch (IOException e) {
1176b5840353SAdam Hornáček             LOGGER.log(Level.WARNING,
1177b5840353SAdam Hornáček                     "An error occurred while piping file " + file + ": ", e);
1178b5840353SAdam Hornáček         }
1179b5840353SAdam Hornáček         return false;
1180b5840353SAdam Hornáček     }
1181b5840353SAdam Hornáček 
1182b5840353SAdam Hornáček     /**
1183b5840353SAdam Hornáček      * Silently dump an xref file to the given destination. All
1184b5840353SAdam Hornáček      * {@link IOException}s get caught and logged, but not re-thrown.
1185b5840353SAdam Hornáček      * @param out dump destination
1186b5840353SAdam Hornáček      * @param file file to dump
1187ddad4e6fSVladimir Kotal      * @param compressed if {@code true} the denoted file is assumed to be gzipped
1188b5840353SAdam Hornáček      * @param contextPath an optional override of "/source/" as the context path
1189b5840353SAdam Hornáček      * @return {@code true} on success (everything read and written)
1190b5840353SAdam Hornáček      * @throws NullPointerException if a parameter is {@code null}.
1191b5840353SAdam Hornáček      */
dumpXref(Writer out, File file, boolean compressed, String contextPath)1192ddad4e6fSVladimir Kotal     public static boolean dumpXref(Writer out, File file, boolean compressed, String contextPath) {
1193ddad4e6fSVladimir Kotal 
1194b5840353SAdam Hornáček         if (!file.exists()) {
1195b5840353SAdam Hornáček             return false;
1196b5840353SAdam Hornáček         }
1197ddad4e6fSVladimir Kotal 
1198ddad4e6fSVladimir Kotal         /*
1199b5840353SAdam Hornáček          * For backward compatibility, read the OpenGrok-produced document
1200b5840353SAdam Hornáček          * using the system default charset.
1201b5840353SAdam Hornáček          */
1202a20d6924SVladimir Kotal         try (InputStream iss = new BufferedInputStream(new FileInputStream(file));
1203a20d6924SVladimir Kotal             Reader in = compressed ? new InputStreamReader(new GZIPInputStream(iss)) : new InputStreamReader(iss)) {
1204b5840353SAdam Hornáček                 dumpXref(out, in, contextPath);
1205b5840353SAdam Hornáček                 return true;
1206ddad4e6fSVladimir Kotal         } catch (IOException e) {
1207ddad4e6fSVladimir Kotal             LOGGER.log(Level.WARNING, "An error occurred while piping file " + file, e);
1208ddad4e6fSVladimir Kotal         }
1209ddad4e6fSVladimir Kotal 
1210b5840353SAdam Hornáček         return false;
1211b5840353SAdam Hornáček     }
1212b5840353SAdam Hornáček 
1213b5840353SAdam Hornáček     /**
1214b5840353SAdam Hornáček      * Silently dump an xref file to the given destination. All
1215b5840353SAdam Hornáček      * {@link IOException}s get caught and logged, but not re-thrown.
1216b5840353SAdam Hornáček      * @param out dump destination
1217b5840353SAdam Hornáček      * @param in source to read
1218b5840353SAdam Hornáček      * @param contextPath an optional override of "/source/" as the context path
1219b5840353SAdam Hornáček      * @throws IOException as defined by the given reader/writer
1220b5840353SAdam Hornáček      * @throws NullPointerException if a parameter is {@code null}.
1221b5840353SAdam Hornáček      */
dumpXref(Writer out, Reader in, String contextPath)1222b5840353SAdam Hornáček     public static void dumpXref(Writer out, Reader in, String contextPath)
1223b5840353SAdam Hornáček             throws IOException {
1224b5840353SAdam Hornáček         if (in == null || out == null) {
1225b5840353SAdam Hornáček             return;
1226b5840353SAdam Hornáček         }
1227b5840353SAdam Hornáček         XrefSourceTransformer xform = new XrefSourceTransformer(in);
1228b5840353SAdam Hornáček         xform.setWriter(out);
1229b5840353SAdam Hornáček         xform.setContextPath(contextPath);
1230b5840353SAdam Hornáček         while (xform.yylex()) {
1231b5840353SAdam Hornáček             // Nothing else to do.
1232b5840353SAdam Hornáček         }
1233b5840353SAdam Hornáček     }
1234b5840353SAdam Hornáček 
1235b5840353SAdam Hornáček     /**
1236b5840353SAdam Hornáček      * Print a row in an HTML table.
1237b5840353SAdam Hornáček      *
1238b5840353SAdam Hornáček      * @param out destination for the HTML output
1239b5840353SAdam Hornáček      * @param cells the values to print in the cells of the row
1240b5840353SAdam Hornáček      * @throws IOException if an error happens while writing to {@code out}
1241b5840353SAdam Hornáček      */
printTableRow(Appendable out, Object... cells)1242b5840353SAdam Hornáček     private static void printTableRow(Appendable out, Object... cells)
1243b5840353SAdam Hornáček             throws IOException {
1244b5840353SAdam Hornáček         out.append("<tr>");
1245b5840353SAdam Hornáček         StringBuilder buf = new StringBuilder(256);
1246b5840353SAdam Hornáček         for (Object cell : cells) {
1247b5840353SAdam Hornáček             out.append("<td>");
1248b5840353SAdam Hornáček             String str = (cell == null) ? "null" : cell.toString();
1249b5840353SAdam Hornáček             htmlize(str, buf);
1250b5840353SAdam Hornáček             out.append(str);
1251b5840353SAdam Hornáček             buf.setLength(0);
1252b5840353SAdam Hornáček             out.append("</td>");
1253b5840353SAdam Hornáček         }
1254b5840353SAdam Hornáček         out.append("</tr>");
1255b5840353SAdam Hornáček     }
1256b5840353SAdam Hornáček 
1257b5840353SAdam Hornáček     /**
1258b5840353SAdam Hornáček      * Print an unordered list (HTML).
1259b5840353SAdam Hornáček      *
1260b5840353SAdam Hornáček      * @param out destination for the HTML output
1261b5840353SAdam Hornáček      * @param items the list items
1262b5840353SAdam Hornáček      * @throws IOException if an error happens while writing to {@code out}
1263b5840353SAdam Hornáček      */
printUnorderedList(Appendable out, Collection<String> items)1264b5840353SAdam Hornáček     private static void printUnorderedList(Appendable out,
1265b5840353SAdam Hornáček             Collection<String> items) throws IOException {
1266b5840353SAdam Hornáček         out.append("<ul>");
1267b5840353SAdam Hornáček         StringBuilder buf = new StringBuilder(256);
1268b5840353SAdam Hornáček         for (String item : items) {
1269b5840353SAdam Hornáček             out.append("<li>");
1270b5840353SAdam Hornáček             htmlize(item, buf);
1271b5840353SAdam Hornáček             out.append(buf);
1272b5840353SAdam Hornáček             buf.setLength(0);
1273b5840353SAdam Hornáček             out.append("</li>");
1274b5840353SAdam Hornáček         }
1275b5840353SAdam Hornáček         out.append("</ul>");
1276b5840353SAdam Hornáček     }
1277b5840353SAdam Hornáček 
1278b5840353SAdam Hornáček     /**
1279b5840353SAdam Hornáček      * Create a string literal for use in JavaScript functions.
1280b5840353SAdam Hornáček      *
1281b5840353SAdam Hornáček      * @param str the string to be represented by the literal
1282b5840353SAdam Hornáček      * @return a JavaScript string literal
1283b5840353SAdam Hornáček      */
jsStringLiteral(String str)1284b5840353SAdam Hornáček     public static String jsStringLiteral(String str) {
1285b5840353SAdam Hornáček         StringBuilder sb = new StringBuilder();
1286b5840353SAdam Hornáček         sb.append('"');
1287b5840353SAdam Hornáček         for (int i = 0; i < str.length(); i++) {
1288b5840353SAdam Hornáček             char c = str.charAt(i);
1289b5840353SAdam Hornáček             switch (c) {
1290b5840353SAdam Hornáček                 case '"':
1291b5840353SAdam Hornáček                     sb.append("\\\"");
1292b5840353SAdam Hornáček                     break;
1293b5840353SAdam Hornáček                 case '\\':
1294b5840353SAdam Hornáček                     sb.append("\\\\");
1295b5840353SAdam Hornáček                     break;
1296b5840353SAdam Hornáček                 case '\n':
1297b5840353SAdam Hornáček                     sb.append("\\n");
1298b5840353SAdam Hornáček                     break;
1299b5840353SAdam Hornáček                 case '\r':
1300b5840353SAdam Hornáček                     sb.append("\\r");
1301b5840353SAdam Hornáček                     break;
1302b5840353SAdam Hornáček                 default:
1303b5840353SAdam Hornáček                     sb.append(c);
1304b5840353SAdam Hornáček             }
1305b5840353SAdam Hornáček         }
1306b5840353SAdam Hornáček         sb.append('"');
1307b5840353SAdam Hornáček         return sb.toString();
1308b5840353SAdam Hornáček     }
1309b5840353SAdam Hornáček 
1310b5840353SAdam Hornáček     /**
1311b5840353SAdam Hornáček      * Make a path relative by stripping off a prefix. If the path does not have
1312b5840353SAdam Hornáček      * the given prefix, return the full path unchanged.
1313b5840353SAdam Hornáček      *
1314b5840353SAdam Hornáček      * @param prefix the prefix to strip off
1315b5840353SAdam Hornáček      * @param fullPath the path from which to remove the prefix
1316b5840353SAdam Hornáček      * @return a path relative to {@code prefix} if {@code prefix} is a parent
1317b5840353SAdam Hornáček      * directory of {@code fullPath}; otherwise, {@code fullPath}
1318b5840353SAdam Hornáček      */
stripPathPrefix(String prefix, String fullPath)1319b5840353SAdam Hornáček     public static String stripPathPrefix(String prefix, String fullPath) {
1320b5840353SAdam Hornáček         // Find the length of the prefix to strip off. The prefix should
1321b5840353SAdam Hornáček         // represent a directory, so it could end with a slash. In case it
1322b5840353SAdam Hornáček         // doesn't end with a slash, increase the length by one so that we
1323b5840353SAdam Hornáček         // strip off the leading slash from the relative path.
1324b5840353SAdam Hornáček         int prefixLength = prefix.length();
1325b5840353SAdam Hornáček         if (!prefix.endsWith("/")) {
1326b5840353SAdam Hornáček             prefixLength++;
1327b5840353SAdam Hornáček         }
1328b5840353SAdam Hornáček 
1329b5840353SAdam Hornáček         // If the full path starts with the prefix, strip off the prefix.
1330b5840353SAdam Hornáček         if (fullPath.length() > prefixLength && fullPath.startsWith(prefix)
1331b5840353SAdam Hornáček                 && fullPath.charAt(prefixLength - 1) == '/') {
1332b5840353SAdam Hornáček             return fullPath.substring(prefixLength);
1333b5840353SAdam Hornáček         }
1334b5840353SAdam Hornáček 
1335b5840353SAdam Hornáček         // Otherwise, return the full path.
1336b5840353SAdam Hornáček         return fullPath;
1337b5840353SAdam Hornáček     }
1338b5840353SAdam Hornáček 
1339b5840353SAdam Hornáček     /**
134091712be4SVladimir Kotal      * Creates a HTML slider for pagination. This has the same effect as
1341b5840353SAdam Hornáček      * invoking <code>createSlider(offset, limit, size, null)</code>.
1342b5840353SAdam Hornáček      *
1343b5840353SAdam Hornáček      * @param offset start of the current page
1344b5840353SAdam Hornáček      * @param limit max number of items per page
1345b5840353SAdam Hornáček      * @param size number of total hits to paginate
1346b5840353SAdam Hornáček      * @return string containing slider html
1347b5840353SAdam Hornáček      */
createSlider(int offset, int limit, int size)1348b5840353SAdam Hornáček     public static String createSlider(int offset, int limit, int size) {
1349b5840353SAdam Hornáček         return createSlider(offset, limit, size, null);
1350b5840353SAdam Hornáček     }
1351b5840353SAdam Hornáček 
1352b5840353SAdam Hornáček     /**
135391712be4SVladimir Kotal      * Creates a HTML slider for pagination.
1354b5840353SAdam Hornáček      *
1355b5840353SAdam Hornáček      * @param offset start of the current page
1356b5840353SAdam Hornáček      * @param limit max number of items per page
1357b5840353SAdam Hornáček      * @param size number of total hits to paginate
1358b5840353SAdam Hornáček      * @param request request containing URL parameters which should be appended
1359b5840353SAdam Hornáček      * to the page URL
1360b5840353SAdam Hornáček      * @return string containing slider html
1361b5840353SAdam Hornáček      */
createSlider(int offset, int limit, long size, HttpServletRequest request)1362b5840353SAdam Hornáček     public static String createSlider(int offset, int limit, long size, HttpServletRequest request) {
1363b5840353SAdam Hornáček         String slider = "";
1364b5840353SAdam Hornáček         if (limit < size) {
1365b5840353SAdam Hornáček             final StringBuilder buf = new StringBuilder(4096);
1366b5840353SAdam Hornáček             int lastPage = (int) Math.ceil((double) size / limit);
1367b5840353SAdam Hornáček             // startingResult is the number of a first result on the current page
1368b5840353SAdam Hornáček             int startingResult = offset - limit * (offset / limit % 10 + 1);
1369b5840353SAdam Hornáček             int myFirstPage = startingResult < 0 ? 1 : startingResult / limit + 1;
1370b5840353SAdam Hornáček             int myLastPage = Math.min(lastPage, myFirstPage + 10 + (myFirstPage == 1 ? 0 : 1));
1371b5840353SAdam Hornáček 
1372b5840353SAdam Hornáček             // function taking the page number and appending the desired content into the final buffer
1373b13c5a0eSAdam Hornacek             Function<Integer, Void> generatePageLink = page -> {
1374b5840353SAdam Hornáček                 int myOffset = Math.max(0, (page - 1) * limit);
1375b5840353SAdam Hornáček                 if (myOffset <= offset && offset < myOffset + limit) {
1376b5840353SAdam Hornáček                     // do not generate anchor for current page
1377b5840353SAdam Hornáček                     buf.append("<span class=\"sel\">").append(page).append("</span>");
1378b5840353SAdam Hornáček                 } else {
1379b5840353SAdam Hornáček                     buf.append("<a class=\"more\" href=\"?");
1380b5840353SAdam Hornáček                     // append request parameters
1381b5840353SAdam Hornáček                     if (request != null && request.getQueryString() != null) {
1382b5840353SAdam Hornáček                         String query = request.getQueryString();
13831c830032SChris Fraire                         query = query.replaceFirst(RE_Q_E_A_A_COUNT_EQ_VAL, "");
13841c830032SChris Fraire                         query = query.replaceFirst(RE_Q_E_A_A_START_EQ_VAL, "");
13851c830032SChris Fraire                         query = query.replaceFirst(RE_A_ANCHOR_Q_E_A_A, "");
1386b5840353SAdam Hornáček                         if (!query.isEmpty()) {
1387b5840353SAdam Hornáček                             buf.append(query);
1388b5840353SAdam Hornáček                             buf.append("&amp;");
1389b5840353SAdam Hornáček                         }
1390b5840353SAdam Hornáček                     }
13911c830032SChris Fraire                     buf.append(QueryParameters.COUNT_PARAM_EQ).append(limit);
1392b5840353SAdam Hornáček                     if (myOffset != 0) {
13931c830032SChris Fraire                         buf.append("&amp;").append(QueryParameters.START_PARAM_EQ).
13941c830032SChris Fraire                                 append(myOffset);
1395b5840353SAdam Hornáček                     }
1396b5840353SAdam Hornáček                     buf.append("\">");
1397b5840353SAdam Hornáček                     // add << or >> if this link would lead to another section
1398b5840353SAdam Hornáček                     if (page == myFirstPage && page != 1) {
1399b5840353SAdam Hornáček                         buf.append("&lt;&lt");
1400b5840353SAdam Hornáček                     } else if (page == myLastPage && myOffset + limit < size) {
1401b5840353SAdam Hornáček                         buf.append("&gt;&gt;");
1402b5840353SAdam Hornáček                     } else {
1403b5840353SAdam Hornáček                         buf.append(page);
1404b5840353SAdam Hornáček                     }
1405b5840353SAdam Hornáček                     buf.append("</a>");
1406b5840353SAdam Hornáček                 }
1407b5840353SAdam Hornáček                 return null;
1408b5840353SAdam Hornáček             };
1409b5840353SAdam Hornáček 
1410b5840353SAdam Hornáček             // slider composition
1411b5840353SAdam Hornáček             if (myFirstPage != 1) {
1412b5840353SAdam Hornáček                 generatePageLink.apply(1);
1413b5840353SAdam Hornáček                 buf.append("<span>...</span>");
1414b5840353SAdam Hornáček             }
1415b5840353SAdam Hornáček             for (int page = myFirstPage; page <= myLastPage; page++) {
1416b5840353SAdam Hornáček                 generatePageLink.apply(page);
1417b5840353SAdam Hornáček             }
1418b5840353SAdam Hornáček             if (myLastPage != lastPage) {
1419b5840353SAdam Hornáček                 buf.append("<span>...</span>");
1420b5840353SAdam Hornáček                 generatePageLink.apply(lastPage);
1421b5840353SAdam Hornáček             }
1422b5840353SAdam Hornáček             return buf.toString();
1423b5840353SAdam Hornáček         }
1424b5840353SAdam Hornáček         return slider;
1425b5840353SAdam Hornáček     }
1426b5840353SAdam Hornáček 
1427b5840353SAdam Hornáček     /**
142891712be4SVladimir Kotal      * Check if the string is a HTTP URL.
1429b5840353SAdam Hornáček      *
1430b5840353SAdam Hornáček      * @param string the string to check
1431b5840353SAdam Hornáček      * @return true if it is http URL, false otherwise
1432b5840353SAdam Hornáček      */
isHttpUri(String string)1433b5840353SAdam Hornáček     public static boolean isHttpUri(String string) {
1434b5840353SAdam Hornáček         URL url;
1435b5840353SAdam Hornáček         try {
1436b5840353SAdam Hornáček             url = new URL(string);
1437b5840353SAdam Hornáček         } catch (MalformedURLException ex) {
1438b5840353SAdam Hornáček             return false;
1439b5840353SAdam Hornáček         }
1440b5840353SAdam Hornáček         return url.getProtocol().equals("http") || url.getProtocol().equals("https");
1441b5840353SAdam Hornáček     }
1442b5840353SAdam Hornáček 
1443e03b934bSAdam Hornáček     protected static final String REDACTED_USER_INFO = "redacted_by_OpenGrok";
144454aac5b5SVladimir Kotal 
1445b5840353SAdam Hornáček     /**
144654aac5b5SVladimir Kotal      * If given path is a URL, return the string representation with the user-info part filtered out.
144754aac5b5SVladimir Kotal      * @param path path to object
144854aac5b5SVladimir Kotal      * @return either the original string or string representation of URL with the user-info part removed
144954aac5b5SVladimir Kotal      */
redactUrl(String path)145054aac5b5SVladimir Kotal     public static String redactUrl(String path) {
145154aac5b5SVladimir Kotal         URL url;
145254aac5b5SVladimir Kotal         try {
145354aac5b5SVladimir Kotal             url = new URL(path);
145454aac5b5SVladimir Kotal         } catch (MalformedURLException e) {
145554aac5b5SVladimir Kotal             // not an URL
145654aac5b5SVladimir Kotal             return path;
145754aac5b5SVladimir Kotal         }
145854aac5b5SVladimir Kotal         if (url.getUserInfo() != null) {
145954aac5b5SVladimir Kotal             return url.toString().replace(url.getUserInfo(),
146054aac5b5SVladimir Kotal                     REDACTED_USER_INFO);
146154aac5b5SVladimir Kotal         } else {
1462a86ae970SVladimir Kotal             return path;
146354aac5b5SVladimir Kotal         }
146454aac5b5SVladimir Kotal     }
146554aac5b5SVladimir Kotal 
146654aac5b5SVladimir Kotal     /**
146754aac5b5SVladimir Kotal      * Build a HTML link to the given HTTP URL. If the URL is not an http URL
1468b5840353SAdam Hornáček      * then it is returned as it was received. This has the same effect as
1469b5840353SAdam Hornáček      * invoking <code>linkify(url, true)</code>.
1470b5840353SAdam Hornáček      *
1471b5840353SAdam Hornáček      * @param url the text to be linkified
1472b5840353SAdam Hornáček      * @return the linkified string
1473b5840353SAdam Hornáček      *
1474b5840353SAdam Hornáček      * @see #linkify(java.lang.String, boolean)
1475b5840353SAdam Hornáček      */
linkify(String url)1476b5840353SAdam Hornáček     public static String linkify(String url) {
1477b5840353SAdam Hornáček         return linkify(url, true);
1478b5840353SAdam Hornáček     }
1479b5840353SAdam Hornáček 
1480b5840353SAdam Hornáček     /**
1481b5840353SAdam Hornáček      * Build a html link to the given http URL. If the URL is not an http URL
1482b5840353SAdam Hornáček      * then it is returned as it was received.
1483b5840353SAdam Hornáček      *
148491712be4SVladimir Kotal      * @param url the HTTP URL
1485b5840353SAdam Hornáček      * @param newTab if the link should open in a new tab
148691712be4SVladimir Kotal      * @return HTML code containing the link &lt;a&gt;...&lt;/a&gt;
1487b5840353SAdam Hornáček      */
linkify(String url, boolean newTab)1488b5840353SAdam Hornáček     public static String linkify(String url, boolean newTab) {
1489b5840353SAdam Hornáček         if (isHttpUri(url)) {
1490b5840353SAdam Hornáček             try {
1491b5840353SAdam Hornáček                 Map<String, String> attrs = new TreeMap<>();
1492b5840353SAdam Hornáček                 attrs.put("href", url);
1493b5840353SAdam Hornáček                 attrs.put("title", String.format("Link to %s", Util.encode(url)));
1494b5840353SAdam Hornáček                 if (newTab) {
1495b5840353SAdam Hornáček                     attrs.put("target", "_blank");
14967b94bd45SAdam Hornacek                     attrs.put("rel", "noreferrer");
1497b5840353SAdam Hornáček                 }
1498b5840353SAdam Hornáček                 return buildLink(url, attrs);
1499b5840353SAdam Hornáček             } catch (URISyntaxException | MalformedURLException ex) {
1500b5840353SAdam Hornáček                 return url;
1501b5840353SAdam Hornáček             }
1502b5840353SAdam Hornáček         }
1503b5840353SAdam Hornáček         return url;
1504b5840353SAdam Hornáček     }
1505b5840353SAdam Hornáček 
1506b5840353SAdam Hornáček     /**
1507b5840353SAdam Hornáček      * Build an anchor with given name and a pack of attributes. Automatically
1508b5840353SAdam Hornáček      * escapes href attributes and automatically escapes the name into HTML
1509b5840353SAdam Hornáček      * entities.
1510b5840353SAdam Hornáček      *
1511b5840353SAdam Hornáček      * @param name displayed name of the anchor
1512b5840353SAdam Hornáček      * @param attrs map of attributes for the html element
1513b5840353SAdam Hornáček      * @return string containing the result
1514b5840353SAdam Hornáček      *
151581b586e6SVladimir Kotal      * @throws URISyntaxException URI syntax
151681b586e6SVladimir Kotal      * @throws MalformedURLException malformed URL
1517b5840353SAdam Hornáček      */
buildLink(String name, Map<String, String> attrs)1518b5840353SAdam Hornáček     public static String buildLink(String name, Map<String, String> attrs)
1519b5840353SAdam Hornáček             throws URISyntaxException, MalformedURLException {
1520b5840353SAdam Hornáček         StringBuilder buffer = new StringBuilder();
1521b5840353SAdam Hornáček         buffer.append("<a");
1522b5840353SAdam Hornáček         for (Entry<String, String> attr : attrs.entrySet()) {
1523b5840353SAdam Hornáček             buffer.append(" ");
1524b5840353SAdam Hornáček             buffer.append(attr.getKey());
1525b5840353SAdam Hornáček             buffer.append("=\"");
1526b5840353SAdam Hornáček             String value = attr.getValue();
1527b5840353SAdam Hornáček             if (attr.getKey().equals("href")) {
1528b5840353SAdam Hornáček                 value = Util.encodeURL(value);
1529b5840353SAdam Hornáček             }
1530b5840353SAdam Hornáček             buffer.append(value);
1531b5840353SAdam Hornáček             buffer.append("\"");
1532b5840353SAdam Hornáček         }
1533b5840353SAdam Hornáček         buffer.append(">");
1534b5840353SAdam Hornáček         buffer.append(Util.htmlize(name));
1535b5840353SAdam Hornáček         buffer.append("</a>");
1536b5840353SAdam Hornáček         return buffer.toString();
1537b5840353SAdam Hornáček     }
1538b5840353SAdam Hornáček 
1539b5840353SAdam Hornáček     /**
1540b5840353SAdam Hornáček      * Build an anchor with given name and a pack of attributes. Automatically
1541b5840353SAdam Hornáček      * escapes href attributes and automatically escapes the name into HTML
1542b5840353SAdam Hornáček      * entities.
1543b5840353SAdam Hornáček      *
1544b5840353SAdam Hornáček      * @param name displayed name of the anchor
1545b5840353SAdam Hornáček      * @param url anchor's URL
1546b5840353SAdam Hornáček      * @return string containing the result
1547b5840353SAdam Hornáček      *
1548b5840353SAdam Hornáček      * @throws URISyntaxException URI syntax
1549b5840353SAdam Hornáček      * @throws MalformedURLException bad URL
1550b5840353SAdam Hornáček      */
buildLink(String name, String url)1551b5840353SAdam Hornáček     public static String buildLink(String name, String url)
1552b5840353SAdam Hornáček             throws URISyntaxException, MalformedURLException {
1553b5840353SAdam Hornáček         Map<String, String> attrs = new TreeMap<>();
1554b5840353SAdam Hornáček         attrs.put("href", url);
1555b5840353SAdam Hornáček         return buildLink(name, attrs);
1556b5840353SAdam Hornáček     }
1557b5840353SAdam Hornáček 
1558b5840353SAdam Hornáček     /**
1559b5840353SAdam Hornáček      * Build an anchor with given name and a pack of attributes. Automatically
1560b5840353SAdam Hornáček      * escapes href attributes and automatically escapes the name into HTML
1561b5840353SAdam Hornáček      * entities.
1562b5840353SAdam Hornáček      *
1563b5840353SAdam Hornáček      * @param name displayed name of the anchor
1564b5840353SAdam Hornáček      * @param url anchor's URL
1565b5840353SAdam Hornáček      * @param newTab a flag if the link should be opened in a new tab
1566b5840353SAdam Hornáček      * @return string containing the result
1567b5840353SAdam Hornáček      *
1568b5840353SAdam Hornáček      * @throws URISyntaxException URI syntax
1569b5840353SAdam Hornáček      * @throws MalformedURLException bad URL
1570b5840353SAdam Hornáček      */
buildLink(String name, String url, boolean newTab)1571b5840353SAdam Hornáček     public static String buildLink(String name, String url, boolean newTab)
1572b5840353SAdam Hornáček             throws URISyntaxException, MalformedURLException {
1573b5840353SAdam Hornáček         Map<String, String> attrs = new TreeMap<>();
1574b5840353SAdam Hornáček         attrs.put("href", url);
1575b5840353SAdam Hornáček         if (newTab) {
1576b5840353SAdam Hornáček             attrs.put("target", "_blank");
15777b94bd45SAdam Hornacek             attrs.put("rel", "noreferrer");
1578b5840353SAdam Hornáček         }
1579b5840353SAdam Hornáček         return buildLink(name, attrs);
1580b5840353SAdam Hornáček     }
1581b5840353SAdam Hornáček 
1582b5840353SAdam Hornáček     /**
1583b5840353SAdam Hornáček      * Replace all occurrences of pattern in the incoming text with the link
1584b5840353SAdam Hornáček      * named name pointing to an URL. It is possible to use the regexp pattern
1585b5840353SAdam Hornáček      * groups in name and URL when they are specified in the pattern.
1586b5840353SAdam Hornáček      *
1587b5840353SAdam Hornáček      * @param text text to replace all patterns
1588b5840353SAdam Hornáček      * @param pattern the pattern to match
1589b5840353SAdam Hornáček      * @param name link display name
1590b5840353SAdam Hornáček      * @param url link URL
1591b5840353SAdam Hornáček      * @return the text with replaced links
1592b5840353SAdam Hornáček      */
linkifyPattern(String text, Pattern pattern, String name, String url)1593b5840353SAdam Hornáček     public static String linkifyPattern(String text, Pattern pattern, String name, String url) {
1594b5840353SAdam Hornáček         try {
1595b5840353SAdam Hornáček             String buildLink = buildLink(name, url, true);
1596b5840353SAdam Hornáček             return pattern.matcher(text).replaceAll(buildLink);
1597b5840353SAdam Hornáček         } catch (URISyntaxException | MalformedURLException ex) {
1598b5840353SAdam Hornáček             LOGGER.log(Level.WARNING, "The given URL ''{0}'' is not valid", url);
1599b5840353SAdam Hornáček             return text;
1600b5840353SAdam Hornáček         }
1601b5840353SAdam Hornáček     }
1602b5840353SAdam Hornáček 
1603b5840353SAdam Hornáček     /**
1604b5840353SAdam Hornáček      * Try to complete the given URL part into full URL with server name, port,
1605b5840353SAdam Hornáček      * scheme, ...
1606b5840353SAdam Hornáček      * <dl>
1607b5840353SAdam Hornáček      * <dt>for request http://localhost:8080/source/xref/xxx and part
1608b5840353SAdam Hornáček      * /cgi-bin/user=</dt>
1609b5840353SAdam Hornáček      * <dd>http://localhost:8080/cgi-bin/user=</dd>
1610b5840353SAdam Hornáček      * <dt>for request http://localhost:8080/source/xref/xxx and part
1611b5840353SAdam Hornáček      * cgi-bin/user=</dt>
1612b5840353SAdam Hornáček      * <dd>http://localhost:8080/source/xref/xxx/cgi-bin/user=</dd>
1613b5840353SAdam Hornáček      * <dt>for request http://localhost:8080/source/xref/xxx and part
1614b5840353SAdam Hornáček      * http://users.com/user=</dt>
1615b5840353SAdam Hornáček      * <dd>http://users.com/user=</dd>
1616b5840353SAdam Hornáček      * </dl>
1617b5840353SAdam Hornáček      *
1618b5840353SAdam Hornáček      * @param url the given URL part, may be already full URL
1619b5840353SAdam Hornáček      * @param req the request containing the information about the server
1620b5840353SAdam Hornáček      * @return the converted URL or the input parameter if there was an error
1621b5840353SAdam Hornáček      */
completeUrl(String url, HttpServletRequest req)1622b5840353SAdam Hornáček     public static String completeUrl(String url, HttpServletRequest req) {
1623b5840353SAdam Hornáček         try {
1624b5840353SAdam Hornáček             if (!isHttpUri(url)) {
1625b5840353SAdam Hornáček                 if (url.startsWith("/")) {
1626b5840353SAdam Hornáček                     return new URI(req.getScheme(), null, req.getServerName(), req.getServerPort(), url, null, null).toString();
1627b5840353SAdam Hornáček                 }
1628b5840353SAdam Hornáček                 StringBuffer prepUrl = req.getRequestURL();
1629b5840353SAdam Hornáček                 if (!url.isEmpty()) {
1630b5840353SAdam Hornáček                     prepUrl.append('/').append(url);
1631b5840353SAdam Hornáček                 }
1632b5840353SAdam Hornáček                 return new URI(prepUrl.toString()).toString();
1633b5840353SAdam Hornáček             }
1634b5840353SAdam Hornáček             return url;
1635b5840353SAdam Hornáček         } catch (URISyntaxException ex) {
1636b5840353SAdam Hornáček             LOGGER.log(Level.INFO,
1637b5840353SAdam Hornáček                     String.format("Unable to convert given URL part '%s' to complete URL", url),
1638b5840353SAdam Hornáček                     ex);
1639b5840353SAdam Hornáček             return url;
1640b5840353SAdam Hornáček         }
1641b5840353SAdam Hornáček     }
1642911e8af0SAdam Hornáček 
1643911e8af0SAdam Hornáček     /**
164491712be4SVladimir Kotal      * Parses the specified URL and returns its query params.
164591712be4SVladimir Kotal      * @param url URL to retrieve the query params from
1646911e8af0SAdam Hornáček      * @return query params of {@code url}
1647911e8af0SAdam Hornáček      */
getQueryParams(final URL url)1648911e8af0SAdam Hornáček     public static Map<String, List<String>> getQueryParams(final URL url) {
1649911e8af0SAdam Hornáček         if (url == null) {
1650911e8af0SAdam Hornáček             throw new IllegalArgumentException("Cannot get query params from the null url");
1651911e8af0SAdam Hornáček         }
1652911e8af0SAdam Hornáček         Map<String, List<String>> returnValue = new HashMap<>();
1653911e8af0SAdam Hornáček 
1654911e8af0SAdam Hornáček         if (url.getQuery() == null) {
1655911e8af0SAdam Hornáček             return returnValue;
1656911e8af0SAdam Hornáček         }
1657911e8af0SAdam Hornáček 
1658911e8af0SAdam Hornáček         String[] pairs = url.getQuery().split("&");
1659911e8af0SAdam Hornáček 
1660911e8af0SAdam Hornáček         for (String pair : pairs) {
1661911e8af0SAdam Hornáček             if (pair.isEmpty()) {
1662911e8af0SAdam Hornáček                 continue;
1663911e8af0SAdam Hornáček             }
1664911e8af0SAdam Hornáček 
1665911e8af0SAdam Hornáček             int idx = pair.indexOf('=');
1666911e8af0SAdam Hornáček             if (idx == -1) {
1667911e8af0SAdam Hornáček                 returnValue.computeIfAbsent(pair, k -> new LinkedList<>());
1668911e8af0SAdam Hornáček                 continue;
1669911e8af0SAdam Hornáček             }
1670911e8af0SAdam Hornáček 
1671f22e1845SAdam Hornacek             String key = URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8);
1672f22e1845SAdam Hornacek             String value = URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8);
1673911e8af0SAdam Hornáček 
1674911e8af0SAdam Hornáček             List<String> paramValues = returnValue.computeIfAbsent(key, k -> new LinkedList<>());
1675911e8af0SAdam Hornáček             paramValues.add(value);
1676911e8af0SAdam Hornáček         }
1677911e8af0SAdam Hornáček         return returnValue;
1678911e8af0SAdam Hornáček     }
1679911e8af0SAdam Hornáček 
1680b5840353SAdam Hornáček }
1681