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

oracle / opengrok / #3670

01 Nov 2023 10:10AM UTC coverage: 74.437% (-0.7%) from 75.16%
#3670

push

web-flow
Fix Sonar codesmell issues (#4460)

Signed-off-by: Gino Augustine <ginoaugustine@gmail.com>

308 of 308 new or added lines in 27 files covered. (100.0%)

43623 of 58604 relevant lines covered (74.44%)

0.74 hits per line

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

69.37
/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.Arrays;
49
import java.util.Collection;
50
import java.util.HashMap;
51
import java.util.LinkedList;
52
import java.util.List;
53
import java.util.Locale;
54
import java.util.Map;
55
import java.util.Map.Entry;
56
import java.util.Optional;
57
import java.util.TreeMap;
58
import java.util.function.IntFunction;
59
import java.util.logging.Level;
60
import java.util.logging.Logger;
61
import java.util.regex.Matcher;
62
import java.util.regex.Pattern;
63
import java.util.zip.GZIPInputStream;
64

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

74
/**
75
 * Class for useful functions.
76
 */
77
public final class Util {
78

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

81
    private static final int BOLD_COUNT_THRESHOLD = 1000;
82

83
    private static final String ANCHOR_LINK_START = "<a href=\"";
84
    private static final String ANCHOR_CLASS_START = "<a class=\"";
85
    private static final String ANCHOR_END = "</a>";
86
    private static final String CLOSE_QUOTED_TAG = "\">";
87
    private static final String SPAN_END = "</span>";
88
    private static final String QUOTE = "&quot;";
89
    private static final String AMP = "&amp;";
90
    private static final String SPRING_BUILDER_EXCEPTIONS = "StringBuilder threw IOException";
91

92
    private static final String RE_Q_ESC_AMP_AMP = "\\?|&amp;|&";
93
    private static final String RE_Q_E_A_A_COUNT_EQ_VAL = "(" + RE_Q_ESC_AMP_AMP + "|\\b)" +
94
            QueryParameters.COUNT_PARAM_EQ + "\\d+";
95
    private static final String RE_Q_E_A_A_START_EQ_VAL = "(" + RE_Q_ESC_AMP_AMP + "|\\b)" +
96
            QueryParameters.START_PARAM_EQ + "\\d+";
97
    private static final String RE_A_ANCHOR_Q_E_A_A = "^(" + RE_Q_ESC_AMP_AMP + ")";
98

99
    /** Private to enforce static. */
100
    private Util() {
101
    }
102

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

119
        StringBuilder sb = new StringBuilder(s.length() * 2);
×
120
        try {
121
            htmlize(s, sb, true);
×
122
        } catch (IOException ioe) {
×
123
            // IOException cannot happen when the destination is a
124
            // StringBuilder. Wrap in an AssertionError so that callers
125
            // don't have to check for an IOException that should never
126
            // happen.
127
            throw new AssertionError(SPRING_BUILDER_EXCEPTIONS, ioe);
×
128
        }
×
129
        return sb.toString();
×
130
    }
131

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

153
    /**
154
     * Append to {@code dest} the UTF-8 URL-encoded representation of the
155
     * Lucene-escaped version of {@code str}.
156
     * @param str a defined instance
157
     * @param dest a defined target
158
     * @throws IOException I/O exception
159
     */
160
    public static void qurlencode(String str, Appendable dest) throws IOException {
161
        uriEncode(QueryParserBase.escape(str), dest);
1✔
162
    }
1✔
163

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

181
        StringBuilder sb = new StringBuilder(s.length() * 2);
1✔
182
        try {
183
            htmlize(s, sb, false);
1✔
184
        } catch (IOException ioe) {
×
185
            // IOException cannot happen when the destination is a
186
            // StringBuilder. Wrap in an AssertionError so that callers
187
            // don't have to check for an IOException that should never
188
            // happen.
189
            throw new AssertionError(SPRING_BUILDER_EXCEPTIONS, ioe);
×
190
        }
1✔
191
        return sb.toString();
1✔
192
    }
193

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

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

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

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

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

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

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

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

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

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

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

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

501
    private static final Pattern EMAIL_PATTERN
1✔
502
            = Pattern.compile("([^<\\s]+@[^>\\s]+)");
1✔
503

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

518
        return email;
×
519
    }
520

521
    /**
522
     * Remove all empty and {@code null} string elements from the given
523
     * <var>names</var> and optionally all redundant information like "." and
524
     * "..".
525
     *
526
     * @param names names to check
527
     * @param canonical if {@code true}, remove redundant elements as well.
528
     * @return a possible empty array of names all with a length &gt; 0.
529
     */
530
    private static String[] normalize(String[] names, boolean canonical) {
531
        LinkedList<String> res = new LinkedList<>();
1✔
532
        if (names == null || names.length == 0) {
1✔
533
            return new String[0];
×
534
        }
535

536
        for (String name : names) {
1✔
537
            if (name == null || name.isEmpty()) {
1✔
538
                continue;
1✔
539
            }
540
            if (canonical) {
1✔
541
                if (name.equals("..")) {
1✔
542
                    res.pollLast();
1✔
543
                } else if (!name.equals(".")) {
1✔
544
                    res.add(name);
1✔
545
                }
546
            } else {
547
                res.add(name);
1✔
548
            }
549
        }
550
        return res.size() == names.length ? names : res.toArray(new String[0]);
1✔
551
    }
552

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

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

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

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

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

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

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

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

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

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

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

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

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

776
            out.write(ANCHOR_END);
×
777

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

963
        if (value != null) {
×
964
            buf.append(AMP).append(key).append('=').append(uriEncode(value));
×
965
        }
966
    }
×
967

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

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

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

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

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

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

1093
        return ret;
1✔
1094
    }
1095

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

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

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

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

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

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

1199
        return false;
×
1200
    }
1201

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

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

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

1231
    }
1232

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1680
        Arrays.stream(url.getQuery().split("&"))
1✔
1681
                .filter(pair -> !pair.isEmpty())
1✔
1682
                .forEach(pair -> {
1✔
1683
                    int idx = pair.indexOf('=');
1✔
1684
                    if (idx == -1) {
1✔
1685
                        returnValue.computeIfAbsent(pair, k -> new LinkedList<>());
1✔
1686
                    } else {
1687
                        var key = URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8);
1✔
1688
                        var value = URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8);
1✔
1689
                        List<String> paramValues = returnValue.computeIfAbsent(key, k -> new LinkedList<>());
1✔
1690
                        paramValues.add(value);
1✔
1691
                    }
1692
                });
1✔
1693
        return returnValue;
1✔
1694
    }
1695

1696
}
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

© 2025 Coveralls, Inc