• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

oracle / opengrok / #3650

24 Oct 2023 03:07PM UTC coverage: 66.012% (-8.4%) from 74.444%
#3650

push

vladak
refactory repository history check

9 of 9 new or added lines in 1 file covered. (100.0%)

38668 of 58577 relevant lines covered (66.01%)

0.66 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

70.03
/opengrok-indexer/src/main/java/org/opengrok/indexer/web/Util.java
1
/*
2
 * CDDL HEADER START
3
 *
4
 * The contents of this file are subject to the terms of the
5
 * Common Development and Distribution License (the "License").
6
 * You may not use this file except in compliance with the License.
7
 *
8
 * See LICENSE.txt included in this distribution for the specific
9
 * language governing permissions and limitations under the License.
10
 *
11
 * When distributing Covered Code, include this CDDL HEADER in each
12
 * file and include the License file at LICENSE.txt.
13
 * If applicable, add the following below this CDDL HEADER, with the
14
 * fields enclosed by brackets "[]" replaced with your own identifying
15
 * information: Portions Copyright [yyyy] [name of copyright owner]
16
 *
17
 * CDDL HEADER END
18
 */
19

20
/*
21
 * Copyright (c) 2005, 2023, Oracle and/or its affiliates. All rights reserved.
22
 * Portions Copyright (c) 2011, Jens Elkner.
23
 * Portions Copyright (c) 2017, 2020, Chris Fraire <cfraire@me.com>.
24
 * Portions Copyright (c) 2019, Krystof Tulinger <k.tulinger@seznam.cz>.
25
 * Portions Copyright (c) 2023, Ric Harris <harrisric@users.noreply.github.com>.
26
 */
27
package org.opengrok.indexer.web;
28

29
import static org.opengrok.indexer.index.Indexer.PATH_SEPARATOR;
30

31
import java.io.BufferedInputStream;
32
import java.io.File;
33
import java.io.FileInputStream;
34
import java.io.IOException;
35
import java.io.InputStream;
36
import java.io.InputStreamReader;
37
import java.io.Reader;
38
import java.io.Writer;
39
import java.net.MalformedURLException;
40
import java.net.URI;
41
import java.net.URISyntaxException;
42
import java.net.URL;
43
import java.net.URLDecoder;
44
import java.net.URLEncoder;
45
import java.nio.charset.StandardCharsets;
46
import java.text.DecimalFormat;
47
import java.text.NumberFormat;
48
import java.util.Collection;
49
import java.util.HashMap;
50
import java.util.LinkedList;
51
import java.util.List;
52
import java.util.Locale;
53
import java.util.Map;
54
import java.util.Map.Entry;
55
import java.util.TreeMap;
56
import java.util.function.Function;
57
import java.util.logging.Level;
58
import java.util.logging.Logger;
59
import java.util.regex.Matcher;
60
import java.util.regex.Pattern;
61
import java.util.zip.GZIPInputStream;
62

63
import jakarta.servlet.http.HttpServletRequest;
64
import org.apache.commons.lang3.SystemUtils;
65
import org.apache.lucene.queryparser.classic.QueryParser;
66
import org.opengrok.indexer.configuration.RuntimeEnvironment;
67
import org.opengrok.indexer.history.Annotation;
68
import org.opengrok.indexer.history.HistoryGuru;
69
import org.opengrok.indexer.logger.LoggerFactory;
70
import org.opengrok.indexer.util.Statistics;
71

72
/**
73
 * Class for useful functions.
74
 */
75
public final class Util {
76

77
    private static final Logger LOGGER = LoggerFactory.getLogger(Util.class);
1✔
78

79
    private static final int BOLD_COUNT_THRESHOLD = 1000;
80

81
    private static final String ANCHOR_LINK_START = "<a href=\"";
82
    private static final String ANCHOR_CLASS_START = "<a class=\"";
83
    private static final String ANCHOR_END = "</a>";
84
    private static final String CLOSE_QUOTED_TAG = "\">";
85

86
    private static final String RE_Q_ESC_AMP_AMP = "\\?|&amp;|&";
87
    private static final String RE_Q_E_A_A_COUNT_EQ_VAL = "(" + RE_Q_ESC_AMP_AMP + "|\\b)" +
88
            QueryParameters.COUNT_PARAM_EQ + "\\d+";
89
    private static final String RE_Q_E_A_A_START_EQ_VAL = "(" + RE_Q_ESC_AMP_AMP + "|\\b)" +
90
            QueryParameters.START_PARAM_EQ + "\\d+";
91
    private static final String RE_A_ANCHOR_Q_E_A_A = "^(" + RE_Q_ESC_AMP_AMP + ")";
92

93
    /** Private to enforce static. */
94
    private Util() {
95
    }
96

97
    /**
98
     * Return a string that represents <code>s</code> in HTML by calling
99
     * {@link #htmlize(java.lang.CharSequence, java.lang.Appendable, boolean)}
100
     * with {@code s}, a transient {@link StringBuilder}, and {@code true}.
101
     * <p>
102
     * (N.b. if no special characters are present, {@code s} is returned as is,
103
     * without the expensive call.)
104
     *
105
     * @param s a defined string
106
     * @return a string representing the character sequence in HTML
107
     */
108
    public static String prehtmlize(String s) {
109
        if (!needsHtmlize(s, true)) {
×
110
            return s;
×
111
        }
112

113
        StringBuilder sb = new StringBuilder(s.length() * 2);
×
114
        try {
115
            htmlize(s, sb, true);
×
116
        } catch (IOException ioe) {
×
117
            // IOException cannot happen when the destination is a
118
            // StringBuilder. Wrap in an AssertionError so that callers
119
            // don't have to check for an IOException that should never
120
            // happen.
121
            throw new AssertionError("StringBuilder threw IOException", ioe);
×
122
        }
×
123
        return sb.toString();
×
124
    }
125

126
    /**
127
     * Calls
128
     * {@link #htmlize(java.lang.CharSequence, java.lang.Appendable, boolean)}
129
     * with {@code q}, a transient {@link StringBuilder}, and {@code true}.
130
     * @param q a character sequence
131
     * @return a string representing the character sequence in HTML
132
     */
133
    public static String prehtmlize(CharSequence q) {
134
        StringBuilder sb = new StringBuilder(q.length() * 2);
×
135
        try {
136
            htmlize(q, sb, true);
×
137
        } catch (IOException ioe) {
×
138
            // IOException cannot happen when the destination is a
139
            // StringBuilder. Wrap in an AssertionError so that callers
140
            // don't have to check for an IOException that should never
141
            // happen.
142
            throw new AssertionError("StringBuilder threw IOException", ioe);
×
143
        }
×
144
        return sb.toString();
×
145
    }
146

147
    /**
148
     * Append to {@code dest} the UTF-8 URL-encoded representation of the
149
     * Lucene-escaped version of {@code str}.
150
     * @param str a defined instance
151
     * @param dest a defined target
152
     * @throws IOException I/O exception
153
     */
154
    public static void qurlencode(String str, Appendable dest) throws IOException {
155
        uriEncode(QueryParser.escape(str), dest);
1✔
156
    }
1✔
157

158
    /**
159
     * Return a string that represents a <code>CharSequence</code> in HTML by
160
     * calling
161
     * {@link #htmlize(java.lang.CharSequence, java.lang.Appendable, boolean)}
162
     * with {@code s}, a transient {@link StringBuilder}, and {@code false}.
163
     * <p>
164
     * (N.b. if no special characters are present, {@code s} is returned as is,
165
     * without the expensive call.)
166
     *
167
     * @param s a defined string
168
     * @return a string representing the character sequence in HTML
169
     */
170
    public static String htmlize(String s) {
171
        if (!needsHtmlize(s, false)) {
1✔
172
            return s;
1✔
173
        }
174

175
        StringBuilder sb = new StringBuilder(s.length() * 2);
1✔
176
        try {
177
            htmlize(s, sb, false);
1✔
178
        } catch (IOException ioe) {
×
179
            // IOException cannot happen when the destination is a
180
            // StringBuilder. Wrap in an AssertionError so that callers
181
            // don't have to check for an IOException that should never
182
            // happen.
183
            throw new AssertionError("StringBuilder threw IOException", ioe);
×
184
        }
1✔
185
        return sb.toString();
1✔
186
    }
187

188
    /**
189
     * Return a string which represents a <code>CharSequence</code> in HTML by
190
     * calling
191
     * {@link #htmlize(java.lang.CharSequence, java.lang.Appendable, boolean)}
192
     * with {@code q}, a transient {@link StringBuilder}, and {@code false}.
193
     *
194
     * @param q a character sequence
195
     * @return a string representing the character sequence in HTML
196
     */
197
    public static String htmlize(CharSequence q) {
198
        StringBuilder sb = new StringBuilder(q.length() * 2);
×
199
        try {
200
            htmlize(q, sb, false);
×
201
        } catch (IOException ioe) {
×
202
            // IOException cannot happen when the destination is a
203
            // StringBuilder. Wrap in an AssertionError so that callers
204
            // don't have to check for an IOException that should never
205
            // happen.
206
            throw new AssertionError("StringBuilder threw IOException", ioe);
×
207
        }
×
208
        return sb.toString();
×
209
    }
210

211
    /**
212
     * Append a character sequence to the given destination whereby special
213
     * characters for HTML or characters that are not printable ASCII are
214
     * escaped accordingly.
215
     *
216
     * @param q a character sequence to escape
217
     * @param dest where to append the character sequence to
218
     * @param pre a value indicating whether the output is pre-formatted -- if
219
     * true then LFs will not be converted to &lt;br&gt; elements
220
     * @throws IOException if an error occurred when writing to {@code dest}
221
     */
222
    public static void htmlize(CharSequence q, Appendable dest, boolean pre)
223
            throws IOException {
224
        for (int i = 0; i < q.length(); i++) {
1✔
225
            htmlize(q.charAt(i), dest, pre);
1✔
226
        }
227
    }
1✔
228

229
    /**
230
     * Calls
231
     * {@link #htmlize(java.lang.CharSequence, java.lang.Appendable, boolean)}
232
     * with {@code q}, {@code dest}, and {@code false}.
233
     *
234
     * @param q a character sequence to escape
235
     * @param dest where to append the character sequence to
236
     * @throws IOException if an error occurred when writing to {@code dest}
237
     */
238
    public static void htmlize(CharSequence q, Appendable dest)
239
            throws IOException {
240
        htmlize(q, dest, false);
1✔
241
    }
1✔
242

243
    /**
244
     * Append a character array to the given destination whereby special
245
     * characters for HTML or characters that are not printable ASCII are
246
     * escaped accordingly.
247
     *
248
     * @param cs characters to escape
249
     * @param length max. number of characters to append, starting from index 0.
250
     * @param dest where to append the character sequence to
251
     * @throws IOException if an error occurred when writing to {@code dest}
252
     */
253
    public static void htmlize(char[] cs, int length, Appendable dest)
254
            throws IOException {
255
        int len = length;
×
256
        if (cs.length < length) {
×
257
            len = cs.length;
×
258
        }
259
        for (int i = 0; i < len; i++) {
×
260
            htmlize(cs[i], dest, false);
×
261
        }
262
    }
×
263

264
    /**
265
     * Append a character to the given destination whereby special characters
266
     * special for HTML or characters that are not printable ASCII are
267
     * escaped accordingly.
268
     *
269
     * @param c the character to append
270
     * @param dest where to append the character to
271
     * @param pre a value indicating whether the output is pre-formatted -- if
272
     * true then LFs will not be converted to &lt;br&gt; elements
273
     * @throws IOException if an error occurred when writing to {@code dest}
274
     * @see #needsHtmlize(char, boolean)
275
     */
276
    private static void htmlize(char c, Appendable dest, boolean pre)
277
            throws IOException {
278
        switch (c) {
1✔
279
            case '\'':
280
                dest.append("&apos;");
1✔
281
                break;
1✔
282
            case '"':
283
                dest.append("&quot;");
1✔
284
                break;
1✔
285
            case '&':
286
                dest.append("&amp;");
1✔
287
                break;
1✔
288
            case '>':
289
                dest.append("&gt;");
1✔
290
                break;
1✔
291
            case '<':
292
                dest.append("&lt;");
1✔
293
                break;
1✔
294
            case '\n':
295
                if (pre) {
1✔
296
                    dest.append(c);
×
297
                } else {
298
                    dest.append("<br/>");
1✔
299
                }
300
                break;
1✔
301
            default:
302
                if ((c >= ' ' && c <= '~') || (c < ' ' &&
1✔
303
                    Character.isWhitespace(c))) {
1✔
304
                    dest.append(c);
1✔
305
                } else {
306
                    dest.append("&#").append(Integer.toString(c)).append(';');
1✔
307
                }
308
                break;
309
        }
310
    }
1✔
311

312
    /**
313
     * Determine if a character is a special character needing HTML escaping or
314
     * is a character that is not printable ASCII.
315
     * @param c the character to examine
316
     * @param pre a value indicating whether the output is pre-formatted -- if
317
     * true then LFs will not be converted to &lt;br&gt; elements
318
     * @see #htmlize(char, java.lang.Appendable, boolean)
319
     */
320
    private static boolean needsHtmlize(char c, boolean pre) {
321
        switch (c) {
1✔
322
            case '\'':
323
            case '"':
324
            case '&':
325
            case '>':
326
            case '<':
327
                return true;
1✔
328
            case '\n':
329
                if (!pre) {
1✔
330
                    return true;
1✔
331
                }
332
            default:
333
                return (c < ' ' || c > '~') && (c >= ' ' || !Character.isWhitespace(c));
1✔
334
        }
335
    }
336

337
    private static boolean needsHtmlize(CharSequence q, boolean pre) {
338
        for (int i = 0; i < q.length(); ++i) {
1✔
339
            if (needsHtmlize(q.charAt(i), pre)) {
1✔
340
                return true;
1✔
341
            }
342
        }
343
        return false;
1✔
344
    }
345

346
    /**
347
     * Convenience method for {@code breadcrumbPath(urlPrefix, path, PATH_SEPARATOR)}.
348
     *
349
     * @param urlPrefix prefix to add to each url
350
     * @param path path to crack
351
     * @return HTML markup for the breadcrumb or the path itself.
352
     *
353
     * @see #breadcrumbPath(String, String, char)
354
     */
355
    public static String breadcrumbPath(String urlPrefix, String path) {
356
        return breadcrumbPath(urlPrefix, path, PATH_SEPARATOR);
1✔
357
    }
358

359
    /**
360
     * Convenience method for
361
     * {@code breadcrumbPath(urlPrefix, path, sep, "", false)}.
362
     *
363
     * @param urlPrefix prefix to add to each url
364
     * @param path path to crack
365
     * @param sep separator to use to crack the given path
366
     *
367
     * @return HTML markup fro the breadcrumb or the path itself.
368
     * @see #breadcrumbPath(String, String, char, String, boolean, boolean)
369
     */
370
    public static String breadcrumbPath(String urlPrefix, String path, char sep) {
371
        return breadcrumbPath(urlPrefix, path, sep, "", false);
1✔
372
    }
373

374
    /**
375
     * Convenience method for
376
     * {@code breadcrumbPath(urlPrefix, path, sep, "", false, path.endsWith(sep)}.
377
     *
378
     * @param urlPrefix prefix to add to each url
379
     * @param path path to crack
380
     * @param sep separator to use to crack the given path
381
     * @param urlPostfix suffix to add to each url
382
     * @param compact if {@code true} the given path gets transformed into its
383
     * canonical form (.i.e. all '.' and '..' and double separators removed, but
384
     * not always resolves to an absolute path) before processing starts.
385
     * @return HTML markup fro the breadcrumb or the path itself.
386
     * @see #breadcrumbPath(String, String, char, String, boolean, boolean)
387
     * @see #getCanonicalPath(String, char)
388
     */
389
    public static String breadcrumbPath(String urlPrefix, String path,
390
            char sep, String urlPostfix, boolean compact) {
391
        if (path == null || path.length() == 0) {
1✔
392
            return path;
1✔
393
        }
394
        return breadcrumbPath(urlPrefix, path, sep, urlPostfix, compact,
1✔
395
                path.charAt(path.length() - 1) == sep);
1✔
396
    }
397

398
    /**
399
     * Create a breadcrumb path to allow navigation to each element of a path.
400
     * Consecutive separators (<var>sep</var>) in the given <var>path</var> are
401
     * always collapsed into a single separator automatically. If
402
     * <var>compact</var> is {@code true} path gets translated into a canonical
403
     * path similar to {@link File#getCanonicalPath()}, however the current
404
     * working directory is assumed to be "/" and no checks are done (e.g.
405
     * neither whether the path [component] exists nor which type it is).
406
     *
407
     * @param urlPrefix what should be prepend to the constructed URL
408
     * @param path the full path from which the breadcrumb path is built.
409
     * @param sep the character that separates the path components in
410
     * <var>path</var>
411
     * @param urlPostfix what should be append to the constructed URL
412
     * @param compact if {@code true}, a canonical path gets constructed before
413
     * processing.
414
     * @param isDir if {@code true} a "/" gets append to the last path
415
     * component's link and <var>sep</var> to its name
416
     * @return <var>path</var> if it resolves to an empty or "/" or {@code null}
417
     * path, the HTML markup for the breadcrumb path otherwise.
418
     */
419
    public static String breadcrumbPath(String urlPrefix, String path,
420
            char sep, String urlPostfix, boolean compact, boolean isDir) {
421
        if (path == null || path.length() == 0) {
1✔
422
            return path;
×
423
        }
424
        String[] pnames = normalize(path.split(escapeForRegex(sep)), compact);
1✔
425
        if (pnames.length == 0) {
1✔
426
            return path;
×
427
        }
428
        String prefix = urlPrefix == null ? "" : urlPrefix;
1✔
429
        String postfix = urlPostfix == null ? "" : urlPostfix;
1✔
430
        StringBuilder pwd = new StringBuilder(path.length() + pnames.length);
1✔
431
        StringBuilder markup
1✔
432
                = new StringBuilder((pnames.length + 3 >> 1) * path.length()
1✔
433
                        + pnames.length
434
                        * (17 + prefix.length() + postfix.length()));
1✔
435
        int k = path.indexOf(pnames[0]);
1✔
436
        if (path.lastIndexOf(sep, k) != -1) {
1✔
437
            pwd.append(PATH_SEPARATOR);
1✔
438
            markup.append(sep);
1✔
439
        }
440
        for (int i = 0; i < pnames.length; i++) {
1✔
441
            pwd.append(uriEncodePath(pnames[i]));
1✔
442
            if (isDir || i < pnames.length - 1) {
1✔
443
                pwd.append(PATH_SEPARATOR);
1✔
444
            }
445
            markup.append(ANCHOR_LINK_START).append(prefix).append(pwd)
1✔
446
                    .append(postfix).append(CLOSE_QUOTED_TAG).append(pnames[i])
1✔
447
                    .append(ANCHOR_END);
1✔
448
            if (isDir || i < pnames.length - 1) {
1✔
449
                markup.append(sep);
1✔
450
            }
451
        }
452
        return markup.toString();
1✔
453
    }
454

455
    /**
456
     * Normalize the given <var>path</var> to its canonical form. I.e. all
457
     * separators (<var>sep</var>) are replaced with a slash ('/'), all double
458
     * slashes are replaced by a single slash, all single dot path components
459
     * (".") of the formed path are removed and all double dot path components
460
     * (".." ) of the formed path are replaced with its parent or '/' if there
461
     * is no parent.
462
     * <p>
463
     * So the difference to {@link File#getCanonicalPath()} is, that this method
464
     * does not hit the disk (just string manipulation), resolves
465
     * <var>path</var>
466
     * always against '/' and thus always returns an absolute path, which may
467
     * actually not exist, and which has a single trailing '/' if the given
468
     * <var>path</var> ends with the given <var>sep</var>.
469
     *
470
     * @param path path to mangle. If not absolute or {@code null}, the current
471
     * working directory is assumed to be '/'.
472
     * @param sep file separator to use to crack <var>path</var> into path
473
     * components
474
     * @return always a canonical path which starts with a '/'.
475
     */
476
    public static String getCanonicalPath(String path, char sep) {
477
        if (path == null || path.length() == 0) {
×
478
            return "/";
×
479
        }
480
        String[] pnames = normalize(path.split(escapeForRegex(sep)), true);
×
481
        if (pnames.length == 0) {
×
482
            return "/";
×
483
        }
484
        StringBuilder buf = new StringBuilder(path.length());
×
485
        buf.append('/');
×
486
        for (String pname : pnames) {
×
487
            buf.append(pname).append('/');
×
488
        }
489
        if (path.charAt(path.length() - 1) != sep) {
×
490
            // since is not a general purpose method. So we waive to handle
491
            // cases like:
492
            // || path.endsWith("/..") || path.endsWith("/.")
493
            buf.setLength(buf.length() - 1);
×
494
        }
495
        return buf.toString();
×
496
    }
497

498
    private static final Pattern EMAIL_PATTERN
1✔
499
            = Pattern.compile("([^<\\s]+@[^>\\s]+)");
1✔
500

501
    /**
502
     * Get email address of the author.
503
     *
504
     * @param author string containing author and possibly email address.
505
     * @return email address of the author or full author string if the author
506
     * string does not contain an email address.
507
     */
508
    public static String getEmail(String author) {
509
        Matcher emailMatcher = EMAIL_PATTERN.matcher(author);
1✔
510
        String email = author;
1✔
511
        if (emailMatcher.find()) {
1✔
512
            email = emailMatcher.group(1).trim();
1✔
513
        }
514

515
        return email;
1✔
516
    }
517

518
    /**
519
     * Remove all empty and {@code null} string elements from the given
520
     * <var>names</var> and optionally all redundant information like "." and
521
     * "..".
522
     *
523
     * @param names names to check
524
     * @param canonical if {@code true}, remove redundant elements as well.
525
     * @return a possible empty array of names all with a length &gt; 0.
526
     */
527
    private static String[] normalize(String[] names, boolean canonical) {
528
        LinkedList<String> res = new LinkedList<>();
1✔
529
        if (names == null || names.length == 0) {
1✔
530
            return new String[0];
×
531
        }
532
        for (String name : names) {
1✔
533
            if (name == null || name.length() == 0) {
1✔
534
                continue;
1✔
535
            }
536
            if (canonical) {
1✔
537
                if (name.equals("..")) {
1✔
538
                    if (!res.isEmpty()) {
1✔
539
                        res.removeLast();
1✔
540
                    }
541
                } else if (!name.equals(".")) {
1✔
542
                    res.add(name);
1✔
543
                }
544
            } else {
545
                res.add(name);
1✔
546
            }
547
        }
548
        return res.size() == names.length ? names : res.toArray(new String[0]);
1✔
549
    }
550

551
    /**
552
     * Generate a regexp that matches the specified character. Escape it in case
553
     * it is a character that has a special meaning in a regexp.
554
     *
555
     * @param c the character that the regexp should match
556
     * @return a six-character string in the form of <code>&#92;u</code><i>hhhh</i>
557
     */
558
    private static String escapeForRegex(char c) {
559
        StringBuilder sb = new StringBuilder(6);
1✔
560
        sb.append("\\u");
1✔
561
        String hex = Integer.toHexString(c);
1✔
562
        sb.append("0".repeat(4 - hex.length()));
1✔
563
        sb.append(hex);
1✔
564
        return sb.toString();
1✔
565
    }
566

567
    private static final NumberFormat FORMATTER = new DecimalFormat("#,###,###,###.#");
1✔
568

569
    private static final NumberFormat COUNT_FORMATTER = new DecimalFormat("#,###,###,###");
1✔
570

571
    /**
572
     * Convert the given size into a human readable string.
573
     *
574
     * NOTE: when changing the output of this function make sure to adapt the
575
     * jQuery tablesorter custom parsers in web/httpheader.jspf
576
     *
577
     * @param num size to convert.
578
     * @return a readable string
579
     */
580
    public static String readableSize(long num) {
581
        float l = num;
1✔
582
        NumberFormat formatter = (NumberFormat) FORMATTER.clone();
1✔
583
        if (l < 1024) {
1✔
584
            return formatter.format(l) + ' '; // for none-dirs append 'B'? ...
1✔
585
        } else if (l < 1048576) {
1✔
586
            return (formatter.format(l / 1024) + " KiB");
1✔
587
        } else if (l < 1073741824) {
1✔
588
            return ("<b>" + formatter.format(l / 1048576) + " MiB</b>");
1✔
589
        } else {
590
            return ("<b>" + formatter.format(l / 1073741824) + " GiB</b>");
1✔
591
        }
592
    }
593

594
    /**
595
     * Convert the specified {@code count} into a human readable string.
596
     * @param count value to convert.
597
     * @return a readable string
598
     */
599
    public static String readableCount(long count) {
600
        return readableCount(count, false);
×
601
    }
602

603
    /**
604
     * Convert the specified {@code count} into a human readable string.
605
     * @param isKnownDirectory a value indicating if {@code count} is known to
606
     *                         be for a directory
607
     * @param count value to convert.
608
     * @return a readable string
609
     */
610
    public static String readableCount(long count, boolean isKnownDirectory) {
611
        NumberFormat formatter = (NumberFormat) COUNT_FORMATTER.clone();
×
612
        if (isKnownDirectory || count < BOLD_COUNT_THRESHOLD) {
×
613
            return formatter.format(count);
×
614
        } else {
615
            return "<b>" + formatter.format(count) + "</b>";
×
616
        }
617
    }
618

619
    /**
620
     * Converts different HTML special characters into their encodings used in
621
     * html.
622
     *
623
     * @param s input text
624
     * @return encoded text for use in &lt;a title=""&gt; tag
625
     */
626
    public static String encode(String s) {
627
        /**
628
         * Make sure that the buffer is long enough to contain the whole string
629
         * with the expanded special characters. We use 1.5*length as a
630
         * heuristic.
631
         */
632
        StringBuilder sb = new StringBuilder((int) Math.max(10, s.length() * 1.5));
1✔
633
        try {
634
            encode(s, sb);
1✔
635
        } catch (IOException ex) {
×
636
            // IOException cannot happen when the destination is a
637
            // StringBuilder. Wrap in an AssertionError so that callers
638
            // don't have to check for an IOException that should never
639
            // happen.
640
            throw new AssertionError("StringBuilder threw IOException", ex);
×
641
        }
1✔
642
        return sb.toString();
1✔
643
    }
644

645
    /**
646
     * Converts different HTML special characters into their encodings used in
647
     * html.
648
     *
649
     * @param s input text
650
     * @param dest appendable destination for appending the encoded characters
651
     * @throws java.io.IOException I/O exception
652
     */
653
    public static void encode(String s, Appendable dest) throws IOException {
654
        for (int i = 0; i < s.length(); i++) {
1✔
655
            char c = s.charAt(i);
1✔
656
            if (c > 127 || c == '"' || c == '<' || c == '>' || c == '&' || c == '\'') {
1✔
657
                // special html characters
658
                dest.append("&#").append("" + (int) c).append(";");
1✔
659
            } else if (c == ' ') {
1✔
660
                // non breaking space
661
                dest.append("&nbsp;");
1✔
662
            } else if (c == '\t') {
1✔
663
                dest.append("&nbsp;&nbsp;&nbsp;&nbsp;");
1✔
664
            } else if (c == '\n') {
1✔
665
                // <br/>
666
                dest.append("&lt;br/&gt;");
1✔
667
            } else {
668
                dest.append(c);
1✔
669
            }
670
        }
671
    }
1✔
672

673
    /**
674
     * Encode URL.
675
     *
676
     * @param urlStr string URL
677
     * @return the encoded URL
678
     * @throws URISyntaxException URI syntax
679
     * @throws MalformedURLException URL malformed
680
     */
681
    public static String encodeURL(String urlStr) throws URISyntaxException, MalformedURLException {
682
        URL url = new URL(urlStr);
1✔
683
        URI constructed = new URI(url.getProtocol(), url.getUserInfo(),
1✔
684
                url.getHost(), url.getPort(),
1✔
685
                url.getPath(), url.getQuery(), url.getRef());
1✔
686
        return constructed.toString();
1✔
687
    }
688

689
    /**
690
     * Write out line information wrt. to the given annotation in the format:
691
     * {@code Linenumber Blame Author} incl. appropriate links.
692
     *
693
     * @param num linenumber to print
694
     * @param out print destination
695
     * @param annotation annotation to use. If {@code null} only the linenumber
696
     * gets printed.
697
     * @param userPageLink see {@link RuntimeEnvironment#getUserPage()}
698
     * @param userPageSuffix see {@link RuntimeEnvironment#getUserPageSuffix()}
699
     * @param project project that is used
700
     * @throws IOException depends on the destination (<var>out</var>).
701
     */
702
    public static void readableLine(int num, Writer out, Annotation annotation,
703
            String userPageLink, String userPageSuffix, String project)
704
            throws IOException {
705
        readableLine(num, out, annotation, userPageLink, userPageSuffix, project, false);
1✔
706
    }
1✔
707

708
    public static void readableLine(int num, Writer out, Annotation annotation, String userPageLink,
709
            String userPageSuffix, String project, boolean skipNewline)
710
            throws IOException {
711
        // this method should go to JFlexXref
712
        String snum = String.valueOf(num);
1✔
713
        if (num > 1 && !skipNewline) {
1✔
714
            out.write("\n");
1✔
715
        }
716
        out.write(ANCHOR_CLASS_START);
1✔
717
        out.write(num % 10 == 0 ? "hl" : "l");
1✔
718
        out.write("\" name=\"");
1✔
719
        out.write(snum);
1✔
720
        out.write("\" href=\"#");
1✔
721
        out.write(snum);
1✔
722
        out.write(CLOSE_QUOTED_TAG);
1✔
723
        out.write(snum);
1✔
724
        out.write(ANCHOR_END);
1✔
725

726
        if (annotation != null) {
1✔
727
            writeAnnotation(num, out, annotation, userPageLink, userPageSuffix, project);
×
728
        }
729
    }
1✔
730

731
    private static void writeAnnotation(int num, Writer out, Annotation annotation, String userPageLink,
732
                                        String userPageSuffix, String project) throws IOException {
733
        String r = annotation.getRevision(num);
×
734
        boolean enabled = annotation.isEnabled(num);
×
735
        out.write("<span class=\"blame\">");
×
736
        if (enabled) {
×
737
            out.write(ANCHOR_CLASS_START);
×
738
            out.write("r title-tooltip");
×
739
            out.write("\" style=\"background-color: ");
×
740
            out.write(annotation.getColors().getOrDefault(r, "inherit"));
×
741
            out.write("\" href=\"");
×
742
            out.write(uriEncode(annotation.getFilename()));
×
743
            out.write("?");
×
744
            out.write(QueryParameters.ANNOTATION_PARAM_EQ_TRUE);
×
745
            out.write("&amp;");
×
746
            out.write(QueryParameters.REVISION_PARAM_EQ);
×
747
            out.write(uriEncode(r));
×
748
            String msg = annotation.getDesc(r);
×
749
            out.write("\" title=\"");
×
750
            if (msg != null) {
×
751
                out.write(Util.encode(msg));
×
752
            }
753
            if (annotation.getFileVersion(r) != 0) {
×
754
                out.write("&lt;br/&gt;version: " + annotation.getFileVersion(r) + "/"
×
755
                        + annotation.getRevisions().size());
×
756
            }
757
            out.write(CLOSE_QUOTED_TAG);
×
758
        }
759
        StringBuilder buf = new StringBuilder();
×
760
        final boolean most_recent_revision = annotation.getFileVersion(r) == annotation.getRevisions().size();
×
761
        // print an asterisk for the most recent revision
762
        if (most_recent_revision) {
×
763
            buf.append("<span class=\"most_recent_revision\">");
×
764
            buf.append('*');
×
765
        }
766
        htmlize(annotation.getRevisionForDisplay(num), buf);
×
767
        if (most_recent_revision) {
×
768
            buf.append("</span>"); // recent revision span
×
769
        }
770
        out.write(buf.toString());
×
771
        buf.setLength(0);
×
772
        if (enabled) {
×
773
            RuntimeEnvironment env = RuntimeEnvironment.getInstance();
×
774

775
            out.write(ANCHOR_END);
×
776

777
            // Write link to search the revision in current project.
778
            out.write(ANCHOR_CLASS_START);
×
779
            out.write("search\" href=\"" + env.getUrlPrefix());
×
780
            out.write(QueryParameters.DEFS_SEARCH_PARAM_EQ);
×
781
            out.write("&amp;");
×
782
            out.write(QueryParameters.REFS_SEARCH_PARAM_EQ);
×
783
            out.write("&amp;");
×
784
            out.write(QueryParameters.PATH_SEARCH_PARAM_EQ);
×
785
            out.write(project);
×
786
            out.write("&amp;");
×
787
            out.write(QueryParameters.HIST_SEARCH_PARAM_EQ);
×
788
            out.write("&quot;");
×
789
            out.write(uriEncode(r));
×
790
            out.write("&quot;&amp;");
×
791
            out.write(QueryParameters.TYPE_SEARCH_PARAM_EQ);
×
792
            out.write("\" title=\"Search history for this revision");
×
793
            out.write(CLOSE_QUOTED_TAG);
×
794
            out.write("S");
×
795
            out.write(ANCHOR_END);
×
796
        }
797
        String a = annotation.getAuthor(num);
×
798
        if (userPageLink == null) {
×
799
            out.write(HtmlConsts.SPAN_A);
×
800
            htmlize(a, buf);
×
801
            out.write(buf.toString());
×
802
            out.write(HtmlConsts.ZSPAN);
×
803
            buf.setLength(0);
×
804
        } else {
805
            out.write(ANCHOR_CLASS_START);
×
806
            out.write("a\" href=\"");
×
807
            out.write(userPageLink);
×
808
            out.write(uriEncode(a));
×
809
            if (userPageSuffix != null) {
×
810
                out.write(userPageSuffix);
×
811
            }
812
            out.write(CLOSE_QUOTED_TAG);
×
813
            htmlize(a, buf);
×
814
            out.write(buf.toString());
×
815
            buf.setLength(0);
×
816
            out.write(ANCHOR_END);
×
817
        }
818
        out.write("</span>");
×
819
    }
×
820

821
    /**
822
     * Generate a string from the given path and date in a way that allows
823
     * stable lexicographic sorting (i.e. gives always the same results) as a
824
     * walk of the file hierarchy. Thus, null character (\u0000) is used both to
825
     * separate directory components and to separate the path from the date.
826
     *
827
     * @param path path to mangle.
828
     * @param date date string to use.
829
     * @return the mangled path.
830
     */
831
    public static String path2uid(String path, String date) {
832
        return path.replace('/', '\u0000') + "\u0000" + date;
1✔
833
    }
834

835
    /**
836
     * The reverse operation for {@link #path2uid(String, String)} - re-creates
837
     * the unmangled path from the given uid.
838
     *
839
     * @param uid uid to unmangle.
840
     * @return the original path.
841
     */
842
    public static String uid2url(String uid) {
843
        String url = uid.replace('\u0000', PATH_SEPARATOR);
1✔
844
        return url.substring(0, url.lastIndexOf(PATH_SEPARATOR)); // remove date from end
1✔
845
    }
846

847
    /**
848
     * Extracts the date embedded in the uid.
849
     *
850
     * @param uid uid
851
     * @return date embedded in the uid
852
     */
853
    public static String uid2date(String uid) {
854
        return uid.substring(uid.lastIndexOf('\u0000') + 1);
×
855
    }
856

857
    /**
858
     * Sanitizes Windows path delimiters (if {@link SystemUtils#IS_OS_WINDOWS}
859
     * is {@code true}) as
860
     * {@link org.opengrok.indexer.index.Indexer#PATH_SEPARATOR} in order not
861
     * to conflict with the Lucene escape character and also so {@code path}
862
     * appears as a correctly formed URI in search results.
863
     * @param path path to fix
864
     * @return fixed path
865
     */
866
    public static String fixPathIfWindows(String path) {
867
        if (path != null && SystemUtils.IS_OS_WINDOWS) {
1✔
868
            return path.replace(File.separatorChar, PATH_SEPARATOR);
1✔
869
        }
870
        return path;
×
871
    }
872

873
    /**
874
     * Write the 'H A D' links. This is used for search results and directory listings.
875
     *
876
     * @param out   writer for producing output
877
     * @param ctxE  URI encoded prefix
878
     * @param entry file/directory name to write
879
     * @throws IOException depends on the destination (<var>out</var>).
880
     */
881
    public static void writeHAD(Writer out, String ctxE, String entry) throws IOException {
882

883
        String downloadPrefixE = ctxE + Prefix.DOWNLOAD_P;
1✔
884
        String xrefPrefixE = ctxE + Prefix.XREF_P;
1✔
885

886
        out.write("<td class=\"q\">");
1✔
887
        File file = new File(RuntimeEnvironment.getInstance().getSourceRootPath(), entry);
1✔
888

889
        // Write the 'H' (History) link.
890
        if (HistoryGuru.getInstance().hasHistory(file)) {
1✔
891
            String histPrefixE = ctxE + Prefix.HIST_L;
1✔
892

893
            out.write(ANCHOR_LINK_START);
1✔
894
            out.write(histPrefixE);
1✔
895
            if (!entry.startsWith("/")) {
1✔
896
                entry = "/" + entry;
×
897
            }
898
            out.write(entry);
1✔
899
            out.write("\" title=\"History\">H</a>");
1✔
900
        }
901

902
        // Write the 'A' (Annotation) link.
903
        if (HistoryGuru.getInstance().hasAnnotation(file)) {
1✔
904
            out.write(" <a href=\"");
1✔
905
            out.write(xrefPrefixE);
1✔
906
            out.write(entry);
1✔
907
            out.write("?");
1✔
908
            out.write(QueryParameters.ANNOTATION_PARAM_EQ_TRUE);
1✔
909
            out.write("\" title=\"Annotate\">A</a> ");
1✔
910
        }
911

912
        // Write the 'D' (Download) link.
913
        if (!file.isDirectory()) {
1✔
914
            out.write("<a href=\"");
1✔
915
            out.write(downloadPrefixE);
1✔
916
            out.write(entry);
1✔
917
            out.write("\" title=\"Download\">D</a>");
1✔
918
        }
919

920
        out.write("</td>");
1✔
921
    }
1✔
922

923
    /**
924
     * Wrapper around UTF-8 URL encoding of a string.
925
     *
926
     * @param q query to be encoded. If {@code null}, an empty string will be used instead.
927
     * @return null if failed, otherwise the encoded string
928
     * @see URLEncoder#encode(String, String)
929
     */
930
    public static String uriEncode(String q) {
931
        return q == null ? "" : URLEncoder.encode(q, StandardCharsets.UTF_8);
1✔
932
    }
933

934
    /**
935
     * Append to {@code dest} the UTF-8 URL-encoded representation of
936
     * {@code str}.
937
     * @param str a defined instance
938
     * @param dest a defined target
939
     * @throws IOException I/O
940
     */
941
    public static void uriEncode(String str, Appendable dest)
942
            throws IOException {
943
        String uenc = uriEncode(str);
1✔
944
        dest.append(uenc);
1✔
945
    }
1✔
946

947
    /**
948
     * Append '&amp;name=value" to the given buffer. If the given
949
     * <var>value</var>
950
     * is {@code null}, this method does nothing.
951
     *
952
     * @param buf where to append the query string
953
     * @param key the name of the parameter to add. Append as is!
954
     * @param value the value for the given parameter. Gets automatically UTF-8
955
     * URL encoded.
956
     * @throws NullPointerException if the given buffer is {@code null}.
957
     * @see #uriEncode(String)
958
     */
959
    public static void appendQuery(StringBuilder buf, String key,
960
            String value) {
961

962
        if (value != null) {
×
963
            buf.append("&amp;").append(key).append('=').append(uriEncode(value));
×
964
        }
965
    }
×
966

967
    /**
968
     * URI encode the given path.
969
     *
970
     * @param path path to encode.
971
     * @return the encoded path.
972
     * @throws NullPointerException if a parameter is {@code null}
973
     */
974
    public static String uriEncodePath(String path) {
975
        // Bug #19188: Ideally, we'd like to use the standard class library to
976
        // encode the paths. We're aware of the following two methods:
977
        //
978
        // 1) URLEncoder.encode() - this method however transforms space to +
979
        // instead of %20 (which is right for HTML form data, but not for
980
        // paths), and it also does not preserve the separator chars (/).
981
        //
982
        // 2) URI.getRawPath() - transforms space and / as expected, but gets
983
        // confused when the path name contains a colon character (it thinks
984
        // parts of the path is schema in that case)
985
        //
986
        // For now, encode manually the way we want it.
987
        StringBuilder sb = new StringBuilder(path.length());
1✔
988
        for (byte b : path.getBytes(StandardCharsets.UTF_8)) {
1✔
989
            // URLEncoder's javadoc says a-z, A-Z, ., -, _ and * are safe
990
            // characters, so we preserve those to make the encoded string
991
            // shorter and easier to read. We also preserve the separator
992
            // chars (/). All other characters are encoded (as UTF-8 byte
993
            // sequences).
994
            if ((b == '/') || (b >= 'a' && b <= 'z')
1✔
995
                    || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9')
996
                    || (b == '.') || (b == '-') || (b == '_') || (b == '*')) {
997
                sb.append((char) b);
1✔
998
            } else {
999
                sb.append('%');
1✔
1000
                int u = b & 0xFF;  // Make the byte value unsigned.
1✔
1001
                if (u <= 0x0F) {
1✔
1002
                    // Add leading zero if required.
1003
                    sb.append('0');
1✔
1004
                }
1005
                sb.append(Integer.toHexString(u).toUpperCase(Locale.ROOT));
1✔
1006
            }
1007
        }
1008
        return sb.toString();
1✔
1009
    }
1010

1011
    /**
1012
     * Escape a string for use as in an HTML attribute value. The returned value
1013
     * is not enclosed in double quotes. The caller needs to add those.
1014
     *
1015
     * @param q string to escape.
1016
     * @return an empty string if a parameter is {@code null}, the mangled
1017
     * string otherwise.
1018
     */
1019
    public static String formQuoteEscape(String q) {
1020
        if (q == null || q.isEmpty()) {
1✔
1021
            return "";
1✔
1022
        }
1023
        StringBuilder sb = new StringBuilder();
1✔
1024
        char c;
1025
        for (int i = 0; i < q.length(); i++) {
1✔
1026
            c = q.charAt(i);
1✔
1027
            switch (c) {
1✔
1028
                case '"':
1029
                    sb.append("&quot;");
1✔
1030
                    break;
1✔
1031
                case '&':
1032
                    sb.append("&amp;");
1✔
1033
                    break;
1✔
1034
                default:
1035
                    sb.append(c);
1✔
1036
                    break;
1037
            }
1038
        }
1039
        return sb.toString();
1✔
1040
    }
1041

1042
    /**
1043
     * Tag changes in the given <var>line1</var> and <var>line2</var>
1044
     * for highlighting. Removed parts are tagged with CSS class {@code d}, new
1045
     * parts are tagged with CSS class {@code a} using a {@code span} element.
1046
     * The input parameters must not have any HTML escapes in them.
1047
     *
1048
     * @param line1 line of the original file
1049
     * @param line2 line of the changed/new file
1050
     * @return the tagged lines (field[0] ~= line1, field[1] ~= line2).
1051
     * @throws NullPointerException if one of the given parameters is
1052
     * {@code null}.
1053
     */
1054
    public static String[] diffline(StringBuilder line1, StringBuilder line2) {
1055
        String[] ret = new String[2];
1✔
1056
        int s = 0;
1✔
1057
        int m = line1.length() - 1;
1✔
1058
        int n = line2.length() - 1;
1✔
1059
        while (s <= m && s <= n && (line1.charAt(s) == line2.charAt(s))) {
1✔
1060
            s++;
1✔
1061
        }
1062

1063
        while (s <= m && s <= n && (line1.charAt(m) == line2.charAt(n))) {
1✔
1064
            m--;
1✔
1065
            n--;
1✔
1066
        }
1067

1068
        // deleted
1069
        if (s <= m) {
1✔
1070
            String sb = Util.htmlize(line1.substring(0, s)) +
1✔
1071
                    HtmlConsts.SPAN_D +
1072
                    Util.htmlize(line1.substring(s, m + 1)) +
1✔
1073
                    HtmlConsts.ZSPAN +
1074
                    Util.htmlize(line1.substring(m + 1, line1.length()));
1✔
1075
            ret[0] = sb;
1✔
1076
        } else {
1✔
1077
            ret[0] = Util.htmlize(line1.toString()); // no change
1✔
1078
        }
1079

1080
        // added
1081
        if (s <= n) {
1✔
1082
            String sb = Util.htmlize(line2.substring(0, s)) +
1✔
1083
                    HtmlConsts.SPAN_A +
1084
                    Util.htmlize(line2.substring(s, n + 1)) +
1✔
1085
                    HtmlConsts.ZSPAN +
1086
                    Util.htmlize(line2.substring(n + 1, line2.length()));
1✔
1087
            ret[1] = sb;
1✔
1088
        } else {
1✔
1089
            ret[1] = Util.htmlize(line2.toString()); // no change
1✔
1090
        }
1091

1092
        return ret;
1✔
1093
    }
1094

1095
    /**
1096
     * Dump the configuration as an HTML table.
1097
     *
1098
     * @param out destination for the HTML output
1099
     * @throws IOException if an error happens while writing to {@code out}
1100
     */
1101
    @SuppressWarnings("boxing")
1102
    public static void dumpConfiguration(Appendable out) throws IOException {
1103
        out.append("<table border=\"1\" width=\"100%\">");
1✔
1104
        out.append("<tr><th>Variable</th><th>Value</th></tr>");
1✔
1105
        RuntimeEnvironment env = RuntimeEnvironment.getInstance();
1✔
1106
        printTableRow(out, "Source root", env.getSourceRootPath());
1✔
1107
        printTableRow(out, "Data root", env.getDataRootPath());
1✔
1108
        printTableRow(out, "CTags", env.getCtags());
1✔
1109
        printTableRow(out, "Bug page", env.getBugPage());
1✔
1110
        printTableRow(out, "Bug pattern", env.getBugPattern());
1✔
1111
        printTableRow(out, "User page", env.getUserPage());
1✔
1112
        printTableRow(out, "User page suffix", env.getUserPageSuffix());
1✔
1113
        printTableRow(out, "Review page", env.getReviewPage());
1✔
1114
        printTableRow(out, "Review pattern", env.getReviewPattern());
1✔
1115
        printTableRow(out, "Using projects", env.isProjectsEnabled());
1✔
1116
        out.append("<tr><td>Ignored files</td><td>");
1✔
1117
        printUnorderedList(out, env.getIgnoredNames().getItems());
1✔
1118
        out.append("</td></tr>");
1✔
1119
        printTableRow(out, "lucene RAM_BUFFER_SIZE_MB", env.getRamBufferSize());
1✔
1120
        printTableRow(out, "Allow leading wildcard in search",
1✔
1121
                env.isAllowLeadingWildcard());
1✔
1122
        printTableRow(out, "History cache", HistoryGuru.getInstance()
1✔
1123
                .getHistoryCacheInfo());
1✔
1124
        printTableRow(out, "Annotation cache", HistoryGuru.getInstance()
1✔
1125
                .getAnnotationCacheInfo());
1✔
1126
        printTableRow(out, "Authorization plugin directory", env.getPluginDirectory());
1✔
1127
        printTableRow(out, "Authorization watchdog directory", env.getPluginDirectory());
1✔
1128
        printTableRow(out, "Authorization watchdog enabled", env.isAuthorizationWatchdog());
1✔
1129
        printTableRow(out, "Authorization stack", "<pre>" +
1✔
1130
                env.getAuthorizationFramework().getStack().hierarchyToString() + "</pre>");
1✔
1131
        out.append("</table>");
1✔
1132
    }
1✔
1133

1134
    /**
1135
     * Just read the given source and dump as is to the given destination.
1136
     * Does nothing, if one or more of the parameters is {@code null}.
1137
     *
1138
     * @param out write destination
1139
     * @param in source to read
1140
     * @throws IOException as defined by the given reader/writer
1141
     * @throws NullPointerException if a parameter is {@code null}.
1142
     */
1143
    public static void dump(Writer out, Reader in) throws IOException {
1144
        if (in == null || out == null) {
1✔
1145
            return;
×
1146
        }
1147

1148
        char[] buf = new char[8192];
1✔
1149
        int len = 0;
1✔
1150
        while ((len = in.read(buf)) >= 0) {
1✔
1151
            out.write(buf, 0, len);
1✔
1152
        }
1153
    }
1✔
1154

1155
    /**
1156
     * Silently dump a file to the given destination. All {@link IOException}s
1157
     * gets caught and logged, but not re-thrown.
1158
     *
1159
     * @param out dump destination
1160
     * @param dir directory, which should contains the file.
1161
     * @param filename the basename of the file to dump.
1162
     * @param compressed if {@code true} the denoted file is assumed to be gzipped.
1163
     * @return {@code true} on success (everything read and written).
1164
     * @throws NullPointerException if a parameter is {@code null}.
1165
     */
1166
    public static boolean dump(Writer out, File dir, String filename, boolean compressed) {
1167
        return dump(out, new File(dir, filename), compressed);
×
1168
    }
1169

1170
    /**
1171
     * Silently dump a file to the given destination. All {@link IOException}s
1172
     * gets caught and logged, but not re-thrown.
1173
     *
1174
     * @param out dump destination
1175
     * @param file file to dump
1176
     * @param compressed if {@code true} the denoted file is assumed to be gzipped
1177
     * @return {@code true} on success (everything read and written)
1178
     * @throws NullPointerException if a parameter is {@code null}
1179
     */
1180
    public static boolean dump(Writer out, File file, boolean compressed) {
1181
        if (!file.exists()) {
1✔
1182
            return false;
×
1183
        }
1184

1185
        /*
1186
         * For backward compatibility, read the OpenGrok-produced document
1187
         * using the system default charset.
1188
         */
1189
        try (InputStream iss = new BufferedInputStream(new FileInputStream(file))) {
1✔
1190
            try (Reader in = compressed ? new InputStreamReader(new GZIPInputStream(iss)) : new InputStreamReader(iss)) {
1✔
1191
                dump(out, in);
1✔
1192
            }
1193
            return true;
1✔
1194
        } catch (IOException e) {
×
1195
            LOGGER.log(Level.WARNING, String.format("An error occurred while piping file '%s': ", file), e);
×
1196
        }
1197

1198
        return false;
×
1199
    }
1200

1201
    /**
1202
     * Silently dump a xref file to the given destination.
1203
     * All {@link IOException}s get caught and logged, but not re-thrown.
1204
     * @param out dump destination
1205
     * @param file file to dump
1206
     * @param compressed if {@code true} the denoted file is assumed to be gzipped
1207
     * @param contextPath an optional override of "/source/" as the context path
1208
     * @return {@code true} on success (everything read and written)
1209
     * @throws NullPointerException if a parameter is {@code null}
1210
     */
1211
    public static boolean dumpXref(Writer out, File file, boolean compressed, String contextPath) {
1212

1213
        if (!file.exists()) {
×
1214
            return false;
×
1215
        }
1216

1217
        /*
1218
         * For backward compatibility, read the OpenGrok-produced document
1219
         * using the system default charset.
1220
         */
1221
        try (InputStream iss = new BufferedInputStream(new FileInputStream(file));
×
1222
            Reader in = compressed ? new InputStreamReader(new GZIPInputStream(iss)) : new InputStreamReader(iss)) {
×
1223
                dumpXref(out, in, contextPath, file);
×
1224
                return true;
×
1225
        } catch (IOException e) {
×
1226
            LOGGER.log(Level.WARNING, String.format("An error occurred while piping file '%s'", file), e);
×
1227
        }
1228

1229
        return false;
×
1230
    }
1231

1232
    /**
1233
     * Dump a xref file to the given destination.
1234
     * @param out dump destination
1235
     * @param in source to read
1236
     * @param contextPath an optional override of "/source/" as the context path
1237
     * @param file file related to the {@code in}, used for logging only
1238
     * @throws IOException as defined by the given reader/writer
1239
     * @throws NullPointerException if a parameter is {@code null}
1240
     */
1241
    public static void dumpXref(Writer out, Reader in, String contextPath, File file) throws IOException {
1242

1243
        if (in == null || out == null) {
×
1244
            return;
×
1245
        }
1246

1247
        Statistics stat = new Statistics();
×
1248
        XrefSourceTransformer xform = new XrefSourceTransformer(in);
×
1249
        xform.setWriter(out);
×
1250
        xform.setContextPath(contextPath);
×
1251
        while (xform.yylex()) {
×
1252
            // Nothing else to do.
1253
        }
1254
        stat.report(LOGGER, Level.FINEST, String.format("dumped xref '%s'", file), "xref.dump.latency");
×
1255
    }
×
1256

1257
    /**
1258
     * Print a row in an HTML table.
1259
     *
1260
     * @param out destination for the HTML output
1261
     * @param cells the values to print in the cells of the row
1262
     * @throws IOException if an error happens while writing to {@code out}
1263
     */
1264
    private static void printTableRow(Appendable out, Object... cells)
1265
            throws IOException {
1266
        out.append("<tr>");
1✔
1267
        StringBuilder buf = new StringBuilder(256);
1✔
1268
        for (Object cell : cells) {
1✔
1269
            out.append("<td>");
1✔
1270
            String str = (cell == null) ? "null" : cell.toString();
1✔
1271
            htmlize(str, buf);
1✔
1272
            out.append(str);
1✔
1273
            buf.setLength(0);
1✔
1274
            out.append("</td>");
1✔
1275
        }
1276
        out.append("</tr>");
1✔
1277
    }
1✔
1278

1279
    /**
1280
     * Print an unordered list (HTML).
1281
     *
1282
     * @param out destination for the HTML output
1283
     * @param items the list items
1284
     * @throws IOException if an error happens while writing to {@code out}
1285
     */
1286
    private static void printUnorderedList(Appendable out,
1287
            Collection<String> items) throws IOException {
1288
        out.append("<ul>");
1✔
1289
        StringBuilder buf = new StringBuilder(256);
1✔
1290
        for (String item : items) {
1✔
1291
            out.append("<li>");
1✔
1292
            htmlize(item, buf);
1✔
1293
            out.append(buf);
1✔
1294
            buf.setLength(0);
1✔
1295
            out.append("</li>");
1✔
1296
        }
1✔
1297
        out.append("</ul>");
1✔
1298
    }
1✔
1299

1300
    /**
1301
     * Create a string literal for use in JavaScript functions.
1302
     *
1303
     * @param str the string to be represented by the literal
1304
     * @return a JavaScript string literal
1305
     */
1306
    public static String jsStringLiteral(String str) {
1307
        StringBuilder sb = new StringBuilder();
1✔
1308
        sb.append('"');
1✔
1309
        for (int i = 0; i < str.length(); i++) {
1✔
1310
            char c = str.charAt(i);
1✔
1311
            switch (c) {
1✔
1312
                case '"':
1313
                    sb.append("\\\"");
1✔
1314
                    break;
1✔
1315
                case '\\':
1316
                    sb.append("\\\\");
1✔
1317
                    break;
1✔
1318
                case '\n':
1319
                    sb.append("\\n");
1✔
1320
                    break;
1✔
1321
                case '\r':
1322
                    sb.append("\\r");
1✔
1323
                    break;
1✔
1324
                default:
1325
                    sb.append(c);
1✔
1326
            }
1327
        }
1328
        sb.append('"');
1✔
1329
        return sb.toString();
1✔
1330
    }
1331

1332
    /**
1333
     * Make a path relative by stripping off a prefix. If the path does not have
1334
     * the given prefix, return the full path unchanged.
1335
     *
1336
     * @param prefix the prefix to strip off
1337
     * @param fullPath the path from which to remove the prefix
1338
     * @return a path relative to {@code prefix} if {@code prefix} is a parent
1339
     * directory of {@code fullPath}; otherwise, {@code fullPath}
1340
     */
1341
    public static String stripPathPrefix(String prefix, String fullPath) {
1342
        // Find the length of the prefix to strip off. The prefix should
1343
        // represent a directory, so it could end with a slash. In case it
1344
        // doesn't end with a slash, increase the length by one so that we
1345
        // strip off the leading slash from the relative path.
1346
        int prefixLength = prefix.length();
1✔
1347
        if (!prefix.endsWith("/")) {
1✔
1348
            prefixLength++;
1✔
1349
        }
1350

1351
        // If the full path starts with the prefix, strip off the prefix.
1352
        if (fullPath.length() > prefixLength && fullPath.startsWith(prefix)
1✔
1353
                && fullPath.charAt(prefixLength - 1) == '/') {
1✔
1354
            return fullPath.substring(prefixLength);
1✔
1355
        }
1356

1357
        // Otherwise, return the full path.
1358
        return fullPath;
1✔
1359
    }
1360

1361
    /**
1362
     * Creates a HTML slider for pagination. This has the same effect as
1363
     * invoking <code>createSlider(offset, limit, size, null)</code>.
1364
     *
1365
     * @param offset start of the current page
1366
     * @param limit max number of items per page
1367
     * @param size number of total hits to paginate
1368
     * @return string containing slider html
1369
     */
1370
    public static String createSlider(int offset, int limit, int size) {
1371
        return createSlider(offset, limit, size, null);
1✔
1372
    }
1373

1374
    /**
1375
     * Creates HTML slider for pagination.
1376
     *
1377
     * @param offset start of the current page
1378
     * @param limit max number of items per page
1379
     * @param size number of total hits to paginate
1380
     * @param request request containing URL parameters which should be appended to the page URL
1381
     * @return string containing slider html
1382
     */
1383
    public static String createSlider(int offset, int limit, long size, HttpServletRequest request) {
1384
        String slider = "";
1✔
1385
        if (limit < size) {
1✔
1386
            final StringBuilder buf = new StringBuilder(4096);
1✔
1387
            int lastPage = (int) Math.ceil((double) size / limit);
1✔
1388
            // startingResult is the number of a first result on the current page
1389
            int startingResult = offset - limit * (offset / limit % 10 + 1);
1✔
1390
            int myFirstPage = startingResult < 0 ? 1 : startingResult / limit + 1;
1✔
1391
            int myLastPage = Math.min(lastPage, myFirstPage + 10 + (myFirstPage == 1 ? 0 : 1));
1✔
1392

1393
            // function taking the page number and appending the desired content into the final buffer
1394
            Function<Integer, Void> generatePageLink = page -> {
1✔
1395
                int myOffset = Math.max(0, (page - 1) * limit);
1✔
1396
                if (myOffset <= offset && offset < myOffset + limit) {
1✔
1397
                    // do not generate anchor for current page
1398
                    buf.append("<span class=\"sel\">").append(page).append("</span>");
1✔
1399
                } else {
1400
                    buf.append("<a class=\"more\" href=\"?");
1✔
1401
                    // append request parameters
1402
                    if (request != null && request.getQueryString() != null) {
1✔
1403
                        String query = request.getQueryString();
×
1404
                        query = query.replaceFirst(RE_Q_E_A_A_COUNT_EQ_VAL, "");
×
1405
                        query = query.replaceFirst(RE_Q_E_A_A_START_EQ_VAL, "");
×
1406
                        query = query.replaceFirst(RE_A_ANCHOR_Q_E_A_A, "");
×
1407
                        if (!query.isEmpty()) {
×
1408
                            buf.append(query);
×
1409
                            buf.append("&amp;");
×
1410
                        }
1411
                    }
1412
                    buf.append(QueryParameters.COUNT_PARAM_EQ).append(limit);
1✔
1413
                    if (myOffset != 0) {
1✔
1414
                        buf.append("&amp;").append(QueryParameters.START_PARAM_EQ).
1✔
1415
                                append(myOffset);
1✔
1416
                    }
1417
                    buf.append("\">");
1✔
1418
                    // add << or >> if this link would lead to another section
1419
                    if (page == myFirstPage && page != 1) {
1✔
1420
                        buf.append("&lt;&lt");
×
1421
                    } else if (page == myLastPage && myOffset + limit < size) {
1✔
1422
                        buf.append("&gt;&gt;");
×
1423
                    } else {
1424
                        buf.append(page);
1✔
1425
                    }
1426
                    buf.append("</a>");
1✔
1427
                }
1428
                return null;
1✔
1429
            };
1430

1431
            // slider composition
1432
            if (myFirstPage != 1) {
1✔
1433
                generatePageLink.apply(1);
×
1434
                buf.append("<span>...</span>");
×
1435
            }
1436
            for (int page = myFirstPage; page <= myLastPage; page++) {
1✔
1437
                generatePageLink.apply(page);
1✔
1438
            }
1439
            if (myLastPage != lastPage) {
1✔
1440
                buf.append("<span>...</span>");
×
1441
                generatePageLink.apply(lastPage);
×
1442
            }
1443
            return buf.toString();
1✔
1444
        }
1445
        return slider;
1✔
1446
    }
1447

1448
    /**
1449
     * Check if the string is a HTTP URL.
1450
     *
1451
     * @param string the string to check
1452
     * @return true if it is http URL, false otherwise
1453
     */
1454
    public static boolean isHttpUri(String string) {
1455
        URL url;
1456
        try {
1457
            url = new URL(string);
1✔
1458
        } catch (MalformedURLException ex) {
1✔
1459
            return false;
1✔
1460
        }
1✔
1461
        return url.getProtocol().equals("http") || url.getProtocol().equals("https");
1✔
1462
    }
1463

1464
    protected static final String REDACTED_USER_INFO = "redacted_by_OpenGrok";
1465

1466
    /**
1467
     * If given path is a URL, return the string representation with the user-info part filtered out.
1468
     * @param path path to object
1469
     * @return either the original string or string representation of URL with the user-info part removed
1470
     */
1471
    public static String redactUrl(String path) {
1472
        URL url;
1473
        try {
1474
            url = new URL(path);
1✔
1475
        } catch (MalformedURLException e) {
1✔
1476
            // not an URL
1477
            return path;
1✔
1478
        }
1✔
1479
        if (url.getUserInfo() != null) {
1✔
1480
            return url.toString().replace(url.getUserInfo(),
1✔
1481
                    REDACTED_USER_INFO);
1482
        } else {
1483
            return path;
1✔
1484
        }
1485
    }
1486

1487
    /**
1488
     * Build a HTML link to the given HTTP URL. If the URL is not an http URL
1489
     * then it is returned as it was received. This has the same effect as
1490
     * invoking <code>linkify(url, true)</code>.
1491
     *
1492
     * @param url the text to be linkified
1493
     * @return the linkified string
1494
     *
1495
     * @see #linkify(java.lang.String, boolean)
1496
     */
1497
    public static String linkify(String url) {
1498
        return linkify(url, true);
1✔
1499
    }
1500

1501
    /**
1502
     * Build a html link to the given http URL. If the URL is not an http URL
1503
     * then it is returned as it was received.
1504
     *
1505
     * @param url the HTTP URL
1506
     * @param newTab if the link should open in a new tab
1507
     * @return HTML code containing the link &lt;a&gt;...&lt;/a&gt;
1508
     */
1509
    public static String linkify(String url, boolean newTab) {
1510
        if (isHttpUri(url)) {
1✔
1511
            try {
1512
                Map<String, String> attrs = new TreeMap<>();
1✔
1513
                attrs.put("href", url);
1✔
1514
                attrs.put("title", String.format("Link to %s", Util.encode(url)));
1✔
1515
                if (newTab) {
1✔
1516
                    attrs.put("target", "_blank");
1✔
1517
                    attrs.put("rel", "noreferrer");
1✔
1518
                }
1519
                return buildLink(url, attrs);
1✔
1520
            } catch (URISyntaxException | MalformedURLException ex) {
×
1521
                return url;
×
1522
            }
1523
        }
1524
        return url;
1✔
1525
    }
1526

1527
    /**
1528
     * Build an anchor with given name and a pack of attributes. Automatically
1529
     * escapes href attributes and automatically escapes the name into HTML
1530
     * entities.
1531
     *
1532
     * @param name displayed name of the anchor
1533
     * @param attrs map of attributes for the html element
1534
     * @return string containing the result
1535
     *
1536
     * @throws URISyntaxException URI syntax
1537
     * @throws MalformedURLException malformed URL
1538
     */
1539
    public static String buildLink(String name, Map<String, String> attrs)
1540
            throws URISyntaxException, MalformedURLException {
1541
        StringBuilder buffer = new StringBuilder();
1✔
1542
        buffer.append("<a");
1✔
1543
        for (Entry<String, String> attr : attrs.entrySet()) {
1✔
1544
            buffer.append(" ");
1✔
1545
            buffer.append(attr.getKey());
1✔
1546
            buffer.append("=\"");
1✔
1547
            String value = attr.getValue();
1✔
1548
            if (attr.getKey().equals("href")) {
1✔
1549
                value = Util.encodeURL(value);
1✔
1550
            }
1551
            buffer.append(value);
1✔
1552
            buffer.append("\"");
1✔
1553
        }
1✔
1554
        buffer.append(">");
1✔
1555
        buffer.append(Util.htmlize(name));
1✔
1556
        buffer.append("</a>");
1✔
1557
        return buffer.toString();
1✔
1558
    }
1559

1560
    /**
1561
     * Build an anchor with given name and a pack of attributes. Automatically
1562
     * escapes href attributes and automatically escapes the name into HTML
1563
     * entities.
1564
     *
1565
     * @param name displayed name of the anchor
1566
     * @param url anchor's URL
1567
     * @return string containing the result
1568
     *
1569
     * @throws URISyntaxException URI syntax
1570
     * @throws MalformedURLException bad URL
1571
     */
1572
    public static String buildLink(String name, String url)
1573
            throws URISyntaxException, MalformedURLException {
1574
        Map<String, String> attrs = new TreeMap<>();
1✔
1575
        attrs.put("href", url);
1✔
1576
        return buildLink(name, attrs);
1✔
1577
    }
1578

1579
    /**
1580
     * Build an anchor with given name and a pack of attributes. Automatically
1581
     * escapes href attributes and automatically escapes the name into HTML
1582
     * entities.
1583
     *
1584
     * @param name displayed name of the anchor
1585
     * @param url anchor's URL
1586
     * @param newTab a flag if the link should be opened in a new tab
1587
     * @return string containing the result
1588
     *
1589
     * @throws URISyntaxException URI syntax
1590
     * @throws MalformedURLException bad URL
1591
     */
1592
    public static String buildLink(String name, String url, boolean newTab)
1593
            throws URISyntaxException, MalformedURLException {
1594
        Map<String, String> attrs = new TreeMap<>();
1✔
1595
        attrs.put("href", url);
1✔
1596
        if (newTab) {
1✔
1597
            attrs.put("target", "_blank");
1✔
1598
            attrs.put("rel", "noreferrer");
1✔
1599
        }
1600
        return buildLink(name, attrs);
1✔
1601
    }
1602

1603
    /**
1604
     * Replace all occurrences of pattern in the incoming text with the link
1605
     * named name pointing to an URL. It is possible to use the regexp pattern
1606
     * groups in name and URL when they are specified in the pattern.
1607
     *
1608
     * @param text text to replace all patterns
1609
     * @param pattern the pattern to match
1610
     * @param name link display name
1611
     * @param url link URL
1612
     * @return the text with replaced links
1613
     */
1614
    public static String linkifyPattern(String text, Pattern pattern, String name, String url) {
1615
        try {
1616
            String buildLink = buildLink(name, url, true);
1✔
1617
            return pattern.matcher(text).replaceAll(buildLink);
1✔
1618
        } catch (URISyntaxException | MalformedURLException ex) {
×
1619
            LOGGER.log(Level.WARNING, "The given URL ''{0}'' is not valid", url);
×
1620
            return text;
×
1621
        }
1622
    }
1623

1624
    /**
1625
     * Try to complete the given URL part into full URL with server name, port,
1626
     * scheme, ...
1627
     * <dl>
1628
     * <dt>for request http://localhost:8080/source/xref/xxx and part
1629
     * /cgi-bin/user=</dt>
1630
     * <dd>http://localhost:8080/cgi-bin/user=</dd>
1631
     * <dt>for request http://localhost:8080/source/xref/xxx and part
1632
     * cgi-bin/user=</dt>
1633
     * <dd>http://localhost:8080/source/xref/xxx/cgi-bin/user=</dd>
1634
     * <dt>for request http://localhost:8080/source/xref/xxx and part
1635
     * http://users.com/user=</dt>
1636
     * <dd>http://users.com/user=</dd>
1637
     * </dl>
1638
     *
1639
     * @param url the given URL part, may be already full URL
1640
     * @param req the request containing the information about the server
1641
     * @return the converted URL or the input parameter if there was an error
1642
     */
1643
    public static String completeUrl(String url, HttpServletRequest req) {
1644
        try {
1645
            if (!isHttpUri(url)) {
1✔
1646
                if (url.startsWith("/")) {
1✔
1647
                    return new URI(req.getScheme(), null, req.getServerName(), req.getServerPort(), url, null, null).toString();
1✔
1648
                }
1649
                StringBuffer prepUrl = req.getRequestURL();
1✔
1650
                if (!url.isEmpty()) {
1✔
1651
                    prepUrl.append('/').append(url);
1✔
1652
                }
1653
                return new URI(prepUrl.toString()).toString();
1✔
1654
            }
1655
            return url;
1✔
1656
        } catch (URISyntaxException ex) {
×
1657
            LOGGER.log(Level.INFO,
×
1658
                    String.format("Unable to convert given URL part '%s' to complete URL", url),
×
1659
                    ex);
1660
            return url;
×
1661
        }
1662
    }
1663

1664
    /**
1665
     * Parses the specified URL and returns its query params.
1666
     * @param url URL to retrieve the query params from
1667
     * @return query params of {@code url}
1668
     */
1669
    public static Map<String, List<String>> getQueryParams(final URL url) {
1670
        if (url == null) {
1✔
1671
            throw new IllegalArgumentException("Cannot get query params from the null url");
1✔
1672
        }
1673
        Map<String, List<String>> returnValue = new HashMap<>();
1✔
1674

1675
        if (url.getQuery() == null) {
1✔
1676
            return returnValue;
1✔
1677
        }
1678

1679
        String[] pairs = url.getQuery().split("&");
1✔
1680

1681
        for (String pair : pairs) {
1✔
1682
            if (pair.isEmpty()) {
1✔
1683
                continue;
1✔
1684
            }
1685

1686
            int idx = pair.indexOf('=');
1✔
1687
            if (idx == -1) {
1✔
1688
                returnValue.computeIfAbsent(pair, k -> new LinkedList<>());
1✔
1689
                continue;
1✔
1690
            }
1691

1692
            String key = URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8);
1✔
1693
            String value = URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8);
1✔
1694

1695
            List<String> paramValues = returnValue.computeIfAbsent(key, k -> new LinkedList<>());
1✔
1696
            paramValues.add(value);
1✔
1697
        }
1698
        return returnValue;
1✔
1699
    }
1700

1701
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc