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

oracle / opengrok / #3630

20 Oct 2023 10:04AM UTC coverage: 65.943% (-9.8%) from 75.715%
#3630

push

web-flow
junit and docker file sonar issue fixes (#4447)

---------

Signed-off-by: Gino Augustine <ginoaugustine@gmail.com>
Co-authored-by: Vladimir Kotal <vladimir.kotal@oracle.com>

49 of 49 new or added lines in 9 files covered. (100.0%)

38645 of 58604 relevant lines covered (65.94%)

0.66 hits per line

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

55.63
/opengrok-web/src/main/java/org/opengrok/web/PageConfig.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) 2011, 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
 */
25
package org.opengrok.web;
26

27
import static org.opengrok.indexer.history.LatestRevisionUtil.getLatestRevision;
28
import static org.opengrok.indexer.index.Indexer.PATH_SEPARATOR;
29
import static org.opengrok.indexer.index.Indexer.PATH_SEPARATOR_STRING;
30

31
import java.io.BufferedReader;
32
import java.io.File;
33
import java.io.FileNotFoundException;
34
import java.io.IOException;
35
import java.io.InputStream;
36
import java.net.URI;
37
import java.net.URISyntaxException;
38
import java.net.URLDecoder;
39
import java.nio.charset.StandardCharsets;
40
import java.nio.file.Paths;
41
import java.security.InvalidParameterException;
42
import java.util.ArrayList;
43
import java.util.Arrays;
44
import java.util.Collections;
45
import java.util.Comparator;
46
import java.util.EnumSet;
47
import java.util.List;
48
import java.util.Objects;
49
import java.util.Set;
50
import java.util.SortedSet;
51
import java.util.TreeSet;
52
import java.util.concurrent.ExecutorService;
53
import java.util.concurrent.Future;
54
import java.util.logging.Level;
55
import java.util.logging.Logger;
56
import java.util.regex.Pattern;
57
import java.util.stream.Collectors;
58

59
import io.micrometer.core.instrument.Counter;
60
import io.micrometer.core.instrument.MeterRegistry;
61
import jakarta.servlet.ServletRequest;
62
import jakarta.servlet.http.Cookie;
63
import jakarta.servlet.http.HttpServletRequest;
64
import jakarta.servlet.http.HttpServletResponse;
65
import jakarta.ws.rs.core.HttpHeaders;
66
import org.jetbrains.annotations.Nullable;
67
import org.jetbrains.annotations.VisibleForTesting;
68
import org.opengrok.indexer.Info;
69
import org.opengrok.indexer.Metrics;
70
import org.opengrok.indexer.analysis.AbstractAnalyzer;
71
import org.opengrok.indexer.analysis.AnalyzerGuru;
72
import org.opengrok.indexer.analysis.ExpandTabsReader;
73
import org.opengrok.indexer.analysis.NullableNumLinesLOC;
74
import org.opengrok.indexer.analysis.StreamSource;
75
import org.opengrok.indexer.authorization.AuthorizationFramework;
76
import org.opengrok.indexer.configuration.Group;
77
import org.opengrok.indexer.configuration.IgnoredNames;
78
import org.opengrok.indexer.configuration.Project;
79
import org.opengrok.indexer.configuration.RuntimeEnvironment;
80
import org.opengrok.indexer.history.Annotation;
81
import org.opengrok.indexer.history.HistoryGuru;
82
import org.opengrok.indexer.logger.LoggerFactory;
83
import org.opengrok.indexer.search.QueryBuilder;
84
import org.opengrok.indexer.search.DirectoryExtraReader;
85
import org.opengrok.indexer.util.ForbiddenSymlinkException;
86
import org.opengrok.indexer.util.IOUtils;
87
import org.opengrok.indexer.util.LineBreaker;
88
import org.opengrok.indexer.util.TandemPath;
89
import org.opengrok.indexer.web.EftarFileReader;
90
import org.opengrok.indexer.web.Laundromat;
91
import org.opengrok.indexer.web.Prefix;
92
import org.opengrok.indexer.web.QueryParameters;
93
import org.opengrok.indexer.web.SearchHelper;
94
import org.opengrok.indexer.web.SortOrder;
95
import org.opengrok.indexer.web.Util;
96
import org.opengrok.indexer.web.messages.MessagesContainer.AcceptedMessage;
97
import org.suigeneris.jrcs.diff.Diff;
98
import org.suigeneris.jrcs.diff.DifferentiationFailedException;
99

100
/**
101
 * A simple container to lazy initialize common vars wrt. a single request. It
102
 * MUST NOT be shared between several requests and
103
 * {@link #cleanup(ServletRequest)} should be called before the page context
104
 * gets destroyed (e.g.when leaving the {@code service} method).
105
 * <p>
106
 * Purpose is to decouple implementation details from web design, so that the
107
 * JSP developer does not need to know every implementation detail and normally
108
 * has to deal with this class/wrapper, only (so some people may like to call
109
 * this class a bean with request scope ;-)). Furthermore, it helps to keep the
110
 * pages (how content gets generated) consistent and to document the request
111
 * parameters used.
112
 * <p>
113
 * General contract for this class (i.e. if not explicitly documented): no
114
 * method of this class changes neither the request nor the response.
115
 *
116
 * @author Jens Elkner
117
 */
118
public final class PageConfig {
119

120
    private static final Logger LOGGER = LoggerFactory.getLogger(PageConfig.class);
1✔
121

122
    // cookie name
123
    public static final String OPEN_GROK_PROJECT = "OpenGrokProject";
124

125
    public static final String DUMMY_REVISION = "unknown";
126

127
    private static final String HISTORY_JSP_ATTR_NAME = "history.jsp-hist";
128

129
    // query parameters
130
    static final String PROJECT_PARAM_NAME = "project";
131
    static final String GROUP_PARAM_NAME = "group";
132
    private static final String DEBUG_PARAM_NAME = "debug";
133

134
    // TODO if still used, get it from the app context
135

136
    private final AuthorizationFramework authFramework;
137
    private RuntimeEnvironment env;
138
    private IgnoredNames ignoredNames;
139
    private String path;
140
    private File resourceFile;
141
    private String resourcePath;
142
    private EftarFileReader eftarReader;
143
    private String sourceRootPath;
144
    private Boolean isDir;
145
    private String uriEncodedPath;
146
    private Prefix prefix;
147
    private String pageTitle;
148
    private String dtag;
149
    private String rev;
150
    private String fragmentIdentifier; // Also settable via match offset translation
151
    private Boolean hasAnnotation;
152
    private Boolean annotate;
153
    private Annotation annotation;
154
    private Boolean hasHistory;
155
    private static final EnumSet<AbstractAnalyzer.Genre> txtGenres
1✔
156
            = EnumSet.of(AbstractAnalyzer.Genre.DATA, AbstractAnalyzer.Genre.PLAIN, AbstractAnalyzer.Genre.HTML);
1✔
157
    private SortedSet<String> requestedProjects;
158
    private String requestedProjectsString;
159
    private List<String> dirFileList;
160
    private QueryBuilder queryBuilder;
161
    private File dataRoot;
162
    private StringBuilder headLines;
163
    /**
164
     * Page java scripts.
165
     */
166
    private final Scripts scripts = new Scripts();
1✔
167

168
    private static final String ATTR_NAME = PageConfig.class.getCanonicalName();
1✔
169
    private HttpServletRequest req;
170

171
    private final ExecutorService executor;
172

173
    /**
174
     * Sets current request's attribute.
175
     *
176
     * @param attr attribute
177
     * @param val value
178
     */
179
    public void setRequestAttribute(String attr, Object val) {
180
        this.req.setAttribute(attr, val);
1✔
181
    }
1✔
182

183
    /**
184
     * Gets current request's attribute.
185
     * @param attr attribute
186
     * @return Object attribute value or null if attribute does not exist
187
     */
188
    public Object getRequestAttribute(String attr) {
189
        return this.req.getAttribute(attr);
1✔
190
    }
191

192
    /**
193
     * @return name of the JSP attribute to store the {@link org.opengrok.indexer.history.History} object.
194
     */
195
    public String getHistoryAttrName() {
196
        return HISTORY_JSP_ATTR_NAME;
×
197
    }
198

199
    /**
200
     * Removes an attribute from the current request.
201
     * @param string the attribute
202
     */
203
    public void removeAttribute(String string) {
204
        req.removeAttribute(string);
×
205
    }
×
206

207
    /**
208
     * Add the given data to the &lt;head&gt; section of the html page to generate.
209
     *
210
     * @param data data to add. It is copied as is, so remember to escape
211
     * special characters ...
212
     */
213
    public void addHeaderData(String data) {
214
        if (data == null || data.length() == 0) {
×
215
            return;
×
216
        }
217
        if (headLines == null) {
×
218
            headLines = new StringBuilder();
×
219
        }
220
        headLines.append(data);
×
221
    }
×
222

223
    /**
224
     * Get addition data, which should be added as is to the &lt;head&gt; section of the HTML page.
225
     *
226
     * @return an empty string if nothing to add, the data otherwise.
227
     */
228
    public String getHeaderData() {
229
        return headLines == null ? "" : headLines.toString();
×
230
    }
231

232
    /**
233
     * Extract file path and revision strings from the URL.
234
     * @param data DiffData object
235
     * @param context context path
236
     * @param filepath file path array (output parameter)
237
     * @return true if the extraction was successful, false otherwise
238
     * (in which case {@link DiffData#errorMsg} will be set)
239
     */
240
    private boolean getFileRevision(DiffData data, String context, String[] filepath) {
241
        /*
242
         * Basically the request URI looks like this:
243
         * http://$site/$webapp/diff/$resourceFile?r1=$fileA@$revA&r2=$fileB@$revB
244
         * The code below extracts file path and revision from the URI.
245
         */
246
        for (int i = 1; i <= 2; i++) {
1✔
247
            String p = req.getParameter(QueryParameters.REVISION_PARAM + i);
1✔
248
            if (p != null) {
1✔
249
                int j = p.lastIndexOf("@");
1✔
250
                if (j != -1) {
1✔
251
                    filepath[i - 1] = p.substring(0, j);
1✔
252
                    data.rev[i - 1] = p.substring(j + 1);
1✔
253
                }
254
            }
255
        }
256

257
        if (data.rev[0] == null || data.rev[1] == null
1✔
258
                || data.rev[0].length() == 0 || data.rev[1].length() == 0
1✔
259
                || data.rev[0].equals(data.rev[1])) {
1✔
260
            data.errorMsg = "Please pick two revisions to compare the changed "
1✔
261
                    + "from the <a href=\"" + context + Prefix.HIST_L
262
                    + getUriEncodedPath() + "\">history</a>";
1✔
263
            return false;
1✔
264
        }
265

266
        return true;
1✔
267
    }
268

269
    /**
270
     * Get all data required to create a diff view w.r.t. to this request in one go.
271
     *
272
     * @return an instance with just enough information to render a sufficient view.
273
     * If not all required parameters were given either they are supplemented
274
     * with reasonable defaults if possible, otherwise the related field(s) are {@code null}.
275
     * {@link DiffData#errorMsg}
276
     * {@code != null} indicates that an error occurred and one should not try to render a view.
277
     */
278
    public DiffData getDiffData() {
279
        DiffData data = new DiffData(getPath().substring(0, getPath().lastIndexOf(PATH_SEPARATOR)),
1✔
280
                Util.htmlize(getResourceFile().getName()));
1✔
281

282
        String srcRoot = getSourceRootPath();
1✔
283
        String context = req.getContextPath();
1✔
284
        String[] filepath = new String[2];
1✔
285

286
        if (!getFileRevision(data, context, filepath)) {
1✔
287
            return data;
1✔
288
        }
289

290
        data.genre = AnalyzerGuru.getGenre(getResourceFile().getName());
1✔
291
        if (data.genre == null || txtGenres.contains(data.genre)) {
1✔
292
            InputStream[] in = new InputStream[2];
1✔
293
            try {
294
                // Get input stream for both older and newer file.
295
                Future<?>[] future = new Future<?>[2];
1✔
296
                for (int i = 0; i < 2; i++) {
1✔
297
                    File f = new File(srcRoot + filepath[i]);
1✔
298
                    final String revision = data.rev[i];
1✔
299
                    future[i] = executor.submit(() -> HistoryGuru.getInstance().
1✔
300
                            getRevision(f.getParent(), f.getName(), revision));
1✔
301
                }
302

303
                for (int i = 0; i < 2; i++) {
1✔
304
                    // The Executor used by given repository will enforce the timeout.
305
                    in[i] = (InputStream) future[i].get();
1✔
306
                    if (in[i] == null) {
1✔
307
                        data.errorMsg = "Unable to get revision "
1✔
308
                                + Util.htmlize(data.rev[i]) + " for file: "
1✔
309
                                + Util.htmlize(getPath());
1✔
310
                        return data;
1✔
311
                    }
312
                }
313

314
                /*
315
                 * If the genre of the older revision cannot be determined,
316
                 * (this can happen if the file was empty), try with newer
317
                 * version.
318
                 */
319
                for (int i = 0; i < 2 && data.genre == null; i++) {
1✔
320
                    try {
321
                        data.genre = AnalyzerGuru.getGenre(in[i]);
×
322
                    } catch (IOException e) {
×
323
                        data.errorMsg = "Unable to determine the file type: "
×
324
                                + Util.htmlize(e.getMessage());
×
325
                    }
×
326
                }
327

328
                if (data.genre != AbstractAnalyzer.Genre.PLAIN && data.genre != AbstractAnalyzer.Genre.HTML) {
1✔
329
                    return data;
×
330
                }
331

332
                ArrayList<String> lines = new ArrayList<>();
1✔
333
                Project p = getProject();
1✔
334
                for (int i = 0; i < 2; i++) {
1✔
335
                    // All files under source root are read with UTF-8 as a default.
336
                    try (BufferedReader br = new BufferedReader(
1✔
337
                        ExpandTabsReader.wrap(IOUtils.createBOMStrippedReader(
1✔
338
                        in[i], StandardCharsets.UTF_8.name()), p))) {
1✔
339
                        String line;
340
                        while ((line = br.readLine()) != null) {
1✔
341
                            lines.add(line);
1✔
342
                        }
343
                        data.file[i] = lines.toArray(new String[0]);
1✔
344
                        lines.clear();
1✔
345
                    }
346
                    in[i] = null;
1✔
347
                }
348
            } catch (Exception e) {
×
349
                data.errorMsg = "Error reading revisions: "
×
350
                        + Util.htmlize(e.getMessage());
×
351
            } finally {
352
                for (int i = 0; i < 2; i++) {
1✔
353
                    IOUtils.close(in[i]);
1✔
354
                }
355
            }
356
            if (data.errorMsg != null) {
1✔
357
                return data;
×
358
            }
359
            try {
360
                data.revision = Diff.diff(data.file[0], data.file[1]);
1✔
361
            } catch (DifferentiationFailedException e) {
×
362
                data.errorMsg = "Unable to get diffs: " + Util.htmlize(e.getMessage());
×
363
            }
1✔
364
            for (int i = 0; i < 2; i++) {
1✔
365
                try {
366
                    URI u = new URI(null, null, null,
1✔
367
                            filepath[i] + "@" + data.rev[i], null);
368
                    data.param[i] = u.getRawQuery();
1✔
369
                } catch (URISyntaxException e) {
×
370
                    LOGGER.log(Level.WARNING, "Failed to create URI: ", e);
×
371
                }
1✔
372
            }
373
            data.full = fullDiff();
1✔
374
            data.type = getDiffType();
1✔
375
        }
376
        return data;
1✔
377
    }
378

379
    /**
380
     * Get the diff display type to use wrt. the request parameter
381
     * {@code format}.
382
     *
383
     * @return {@link DiffType#SIDEBYSIDE} if the request contains no such
384
     * parameter or one with an unknown value, the recognized diff type
385
     * otherwise.
386
     * @see DiffType#get(String)
387
     * @see DiffType#getAbbrev()
388
     * @see DiffType#toString()
389
     */
390
    public DiffType getDiffType() {
391
        DiffType d = DiffType.get(req.getParameter(QueryParameters.FORMAT_PARAM));
1✔
392
        return d == null ? DiffType.SIDEBYSIDE : d;
1✔
393
    }
394

395
    /**
396
     * Check, whether a full diff should be displayed.
397
     *
398
     * @return {@code true} if a request parameter {@code full} with the literal
399
     * value {@code 1} was found.
400
     */
401
    public boolean fullDiff() {
402
        String val = req.getParameter(QueryParameters.DIFF_LEVEL_PARAM);
1✔
403
        return val != null && val.equals("1");
1✔
404
    }
405

406
    /**
407
     * Check, whether the request contains minimal information required to
408
     * produce a valid page. If this method returns an empty string, the
409
     * referred file or directory actually exists below the source root
410
     * directory and is readable.
411
     *
412
     * @return {@code null} if the referred src file, directory or history is
413
     * not available, an empty String if further processing is ok and a
414
     * non-empty string which contains the URI encoded redirect path if the
415
     * request should be redirected.
416
     * @see #resourceNotAvailable()
417
     * @see #getDirectoryRedirect()
418
     */
419
    public String canProcess() {
420
        if (resourceNotAvailable()) {
1✔
421
            return null;
1✔
422
        }
423
        String redir = getDirectoryRedirect();
1✔
424
        if (redir == null && getPrefix() == Prefix.HIST_L && !hasHistory()) {
1✔
425
            return null;
×
426
        }
427
        // jel: outfactored from list.jsp - seems to be bogus
428
        if (isDir()) {
1✔
429
            if (getPrefix() == Prefix.XREF_P) {
1✔
430
                if (getResourceFileList().isEmpty()
1✔
431
                        && !getRequestedRevision().isEmpty() && !hasHistory()) {
×
432
                    return null;
×
433
                }
434
            } else if ((getPrefix() == Prefix.RAW_P)
1✔
435
                    || (getPrefix() == Prefix.DOWNLOAD_P)) {
1✔
436
                return null;
×
437
            }
438
        }
439
        return redir == null ? "" : redir;
1✔
440
    }
441

442
    /**
443
     * Get a list of filenames in the requested path.
444
     *
445
     * @return an empty list, if the resource does not exist, is not a directory
446
     * or an error occurred when reading it, otherwise a list of filenames in
447
     * that directory, sorted alphabetically
448
     * <p>
449
     * For the root directory (/xref/), authorization check is performed for each
450
     * project in case that projects are used.
451
     * </p>
452
     *
453
     * @see #getResourceFile()
454
     * @see #isDir()
455
     */
456
    public List<String> getResourceFileList() {
457
        if (dirFileList == null) {
1✔
458
            File[] files = null;
1✔
459
            if (isDir() && getResourcePath().length() > 1) {
1✔
460
                files = getResourceFile().listFiles();
1✔
461
            }
462

463
            if (files == null) {
1✔
464
                dirFileList = Collections.emptyList();
×
465
            } else {
466
                List<String> listOfFiles = getSortedFiles(files);
1✔
467

468
                if (env.hasProjects() && getPath().isEmpty()) {
1✔
469
                    /*
470
                     * This denotes the source root directory, we need to filter
471
                     * projects which aren't allowed by the authorization
472
                     * because otherwise the main xref page expose the names of
473
                     * all projects in OpenGrok even those which aren't allowed
474
                     * for the particular user. E.g. remove all which aren't
475
                     * among the filtered set of projects.
476
                     *
477
                     * The authorization check is made in
478
                     * {@link ProjectHelper#getAllProjects()} as a part of all
479
                     * projects filtering.
480
                     */
481
                    List<String> modifiableListOfFiles = new ArrayList<>(listOfFiles);
1✔
482
                    modifiableListOfFiles.removeIf(t -> getProjectHelper().getAllProjects().stream()
1✔
483
                            .noneMatch(p -> p.getName().equalsIgnoreCase(t)));
1✔
484
                    return dirFileList = Collections.unmodifiableList(modifiableListOfFiles);
1✔
485
                }
486

487
                dirFileList = Collections.unmodifiableList(listOfFiles);
1✔
488
            }
489
        }
490
        return dirFileList;
1✔
491
    }
492

493
    private static Comparator<File> getFileComparator() {
494
        if (RuntimeEnvironment.getInstance().getListDirsFirst()) {
1✔
495
            return (f1, f2) -> {
1✔
496
                if (f1.isDirectory() && !f2.isDirectory()) {
1✔
497
                    return -1;
1✔
498
                } else if (!f1.isDirectory() && f2.isDirectory()) {
1✔
499
                    return 1;
1✔
500
                } else {
501
                    return f1.getName().compareTo(f2.getName());
1✔
502
                }
503
            };
504
        } else {
505
            return Comparator.comparing(File::getName);
×
506
        }
507
    }
508

509
    @VisibleForTesting
510
    public static List<String> getSortedFiles(File[] files) {
511
        return Arrays.stream(files).sorted(getFileComparator()).map(File::getName).collect(Collectors.toList());
1✔
512
    }
513

514
    /**
515
     * @return the last modification time of the related file or directory.
516
     * @see File#lastModified()
517
     */
518
    public long getLastModified() {
519
        return getResourceFile().lastModified();
×
520
    }
521

522
    /**
523
     * Get all RSS related directories from the request using its {@code also} parameter.
524
     *
525
     * @return an empty string if the requested resource is not a directory, a
526
     * space (<code>' '</code>) separated list of unchecked directory names otherwise.
527
     */
528
    public String getHistoryDirs() {
529
        if (!isDir()) {
×
530
            return "";
×
531
        }
532
        String[] val = req.getParameterValues("also");
×
533
        if (val == null || val.length == 0) {
×
534
            return getPath();
×
535
        }
536
        StringBuilder paths = new StringBuilder(getPath());
×
537
        for (String val1 : val) {
×
538
            paths.append(' ').append(val1);
×
539
        }
540
        return paths.toString();
×
541
    }
542

543
    /**
544
     * Get the integer value of the given request parameter.
545
     *
546
     * @param name name of the parameter to lookup.
547
     * @param defaultValue value to return, if the parameter is not set, is not a number, or is &lt; 0.
548
     * @return the parsed integer value on success, the given default value otherwise.
549
     */
550
    public int getIntParam(String name, int defaultValue) {
551
        int ret = defaultValue;
1✔
552
        String s = req.getParameter(name);
1✔
553
        if (s != null && s.length() != 0) {
1✔
554
            try {
555
                int x = Integer.parseInt(s, 10);
1✔
556
                if (x >= 0) {
1✔
557
                    ret = x;
1✔
558
                }
559
            } catch (NumberFormatException e) {
1✔
560
                LOGGER.log(Level.INFO, String.format("Failed to parse %s integer %s", name, s), e);
1✔
561
            }
1✔
562
        }
563
        return ret;
1✔
564
    }
565

566
    /**
567
     * Get the <b>start</b> index for a search result or history listing to return by looking up
568
     * the {@code start} request parameter.
569
     *
570
     * @return 0 if the corresponding start parameter is not set or not a number, the number found otherwise.
571
     */
572
    public int getStartIndex() {
573
        return getIntParam(QueryParameters.START_PARAM, 0);
1✔
574
    }
575

576
    /**
577
     * Get the number of search results or history entries to display by looking up the
578
     * {@code n} request parameter.
579
     *
580
     * @return the default number of items if the corresponding start parameter
581
     * is not set or not a number, the number found otherwise.
582
     */
583
    public int getMaxItems() {
584
        return getIntParam(QueryParameters.COUNT_PARAM, getEnv().getHitsPerPage());
1✔
585
    }
586

587
    public int getRevisionMessageCollapseThreshold() {
588
        return getEnv().getRevisionMessageCollapseThreshold();
×
589
    }
590

591
    public int getCurrentIndexedCollapseThreshold() {
592
        return getEnv().getCurrentIndexedCollapseThreshold();
×
593
    }
594

595
    public int getGroupsCollapseThreshold() {
596
        return getEnv().getGroupsCollapseThreshold();
×
597
    }
598

599
    /**
600
     * Get sort orders from the request parameter {@code sort} and if this list
601
     * would be empty from the cookie {@code OpenGrokSorting}.
602
     *
603
     * @return a possible empty list which contains the sort order values in the
604
     * same order supplied by the request parameter or cookie(s).
605
     */
606
    public List<SortOrder> getSortOrder() {
607
        List<SortOrder> sort = new ArrayList<>();
×
608
        List<String> vals = getParameterValues(QueryParameters.SORT_PARAM);
×
609
        for (String s : vals) {
×
610
            SortOrder so = SortOrder.get(s);
×
611
            if (so != null) {
×
612
                sort.add(so);
×
613
            }
614
        }
×
615
        if (sort.isEmpty()) {
×
616
            vals = getCookieVals("OpenGrokSorting");
×
617
            for (String s : vals) {
×
618
                SortOrder so = SortOrder.get(s);
×
619
                if (so != null) {
×
620
                    sort.add(so);
×
621
                }
622
            }
×
623
        }
624
        return sort;
×
625
    }
626

627
    /**
628
     * Get a reference to the {@code QueryBuilder} wrt. to the current request
629
     * parameters: <dl> <dt>q</dt> <dd>freetext lookup rules</dd> <dt>defs</dt>
630
     * <dd>definitions lookup rules</dd> <dt>path</dt> <dd>path related
631
     * rules</dd> <dt>hist</dt> <dd>history related rules</dd> </dl>
632
     *
633
     * @return a query builder with all relevant fields populated.
634
     */
635
    public QueryBuilder getQueryBuilder() {
636
        if (queryBuilder == null) {
1✔
637
            queryBuilder = new QueryBuilder().
1✔
638
                    setFreetext(Laundromat.launderQuery(req.getParameter(QueryBuilder.FULL)))
1✔
639
                    .setDefs(Laundromat.launderQuery(req.getParameter(QueryBuilder.DEFS)))
1✔
640
                    .setRefs(Laundromat.launderQuery(req.getParameter(QueryBuilder.REFS)))
1✔
641
                    .setPath(Laundromat.launderQuery(req.getParameter(QueryBuilder.PATH)))
1✔
642
                    .setHist(Laundromat.launderQuery(req.getParameter(QueryBuilder.HIST)))
1✔
643
                    .setType(Laundromat.launderQuery(req.getParameter(QueryBuilder.TYPE)));
1✔
644
        }
645

646
        return queryBuilder;
1✔
647
    }
648

649
    /**
650
     * Get the <i>Eftar</i> reader for the data directory. If it has been already
651
     * opened and not closed, this instance gets returned. One should not close
652
     * it once used: {@link #cleanup(ServletRequest)} takes care to close it.
653
     *
654
     * @return {@code null} if a reader can't be established, the reader
655
     * otherwise.
656
     */
657
    public EftarFileReader getEftarReader() {
658
        if (eftarReader == null || eftarReader.isClosed()) {
1✔
659
            File f = getEnv().getDtagsEftar();
1✔
660
            if (f == null) {
1✔
661
                eftarReader = null;
1✔
662
            } else {
663
                try {
664
                    eftarReader = new EftarFileReader(f);
×
665
                } catch (FileNotFoundException e) {
×
666
                    LOGGER.log(Level.FINE, "Failed to create EftarFileReader: ", e);
×
667
                }
×
668
            }
669
        }
670
        return eftarReader;
1✔
671
    }
672

673
    /**
674
     * Get the definition tag for the request related file or directory.
675
     *
676
     * @return an empty string if not found, the tag otherwise.
677
     */
678
    public String getDefineTagsIndex() {
679
        if (dtag != null) {
×
680
            return dtag;
×
681
        }
682
        getEftarReader();
×
683
        if (eftarReader != null) {
×
684
            try {
685
                dtag = eftarReader.get(getPath());
×
686
            } catch (IOException e) {
×
687
                LOGGER.log(Level.INFO, "Failed to get entry from eftar reader: ", e);
×
688
            }
×
689
        }
690
        if (dtag == null) {
×
691
            dtag = "";
×
692
        }
693
        return dtag;
×
694
    }
695

696
    /**
697
     * Get the revision parameter {@code r} from the request.
698
     *
699
     * @return revision if found, an empty string otherwise.
700
     */
701
    public String getRequestedRevision() {
702
        if (rev == null) {
1✔
703
            String tmp = Laundromat.launderInput(req.getParameter(QueryParameters.REVISION_PARAM));
1✔
704
            rev = (tmp != null && tmp.length() > 0) ? tmp : "";
1✔
705
        }
706
        return rev;
1✔
707
    }
708

709
    /**
710
     * Check, whether the request related resource has history information.
711
     *
712
     * @return {@code true} if history is available.
713
     * @see HistoryGuru#hasHistory(File)
714
     */
715
    public boolean hasHistory() {
716
        if (hasHistory == null) {
1✔
717
            hasHistory = HistoryGuru.getInstance().hasHistory(getResourceFile());
1✔
718
        }
719
        return hasHistory;
1✔
720
    }
721

722
    /**
723
     * Check, whether annotations are available for the related resource.
724
     *
725
     * @return {@code true} if annotations are available.
726
     */
727
    public boolean hasAnnotations() {
728
        if (hasAnnotation == null) {
1✔
729
            hasAnnotation = !isDir() && HistoryGuru.getInstance().hasAnnotation(getResourceFile());
1✔
730
        }
731
        return hasAnnotation;
1✔
732
    }
733

734
    /**
735
     * Check, whether the resource to show should be annotated.
736
     *
737
     * @return {@code true} if annotation is desired and available.
738
     */
739
    public boolean annotate() {
740
        if (annotate == null) {
1✔
741
            annotate = hasAnnotations() && Boolean.parseBoolean(req.getParameter(QueryParameters.ANNOTATION_PARAM));
1✔
742
        }
743
        return annotate;
1✔
744
    }
745

746
    /**
747
     * Get the annotation for the requested resource.
748
     *
749
     * @return {@code null} if not available or annotation was not requested,
750
     * the cached annotation otherwise.
751
     */
752
    public Annotation getAnnotation() {
753
        if (isDir() || getResourcePath().equals("/") || !annotate()) {
1✔
754
            return null;
×
755
        }
756
        if (annotation != null) {
1✔
757
            return annotation;
×
758
        }
759
        getRequestedRevision();
1✔
760
        try {
761
            annotation = HistoryGuru.getInstance().annotate(resourceFile, rev.isEmpty() ? null : rev);
1✔
762
        } catch (IOException e) {
×
763
            LOGGER.log(Level.WARNING, "Failed to get annotations: ", e);
×
764
            /* ignore */
765
        }
1✔
766
        return annotation;
1✔
767
    }
768

769
    /**
770
     * Get the {@code path} parameter and display value for "Search only in" option.
771
     *
772
     * @return always an array of 3 fields, whereby field[0] contains the path
773
     * value to use (starts and ends always with a {@link org.opengrok.indexer.index.Indexer#PATH_SEPARATOR}).
774
     * Field[1] the contains string to show in the UI. field[2] is set to {@code disabled=""} if the
775
     * current path is the "/" directory, otherwise set to an empty string.
776
     */
777
    public String[] getSearchOnlyIn() {
778
        if (isDir()) {
×
779
            return getPath().length() == 0
×
780
                    ? new String[]{"/", "this directory", "disabled=\"\""}
×
781
                    : new String[]{getPath(), "this directory", ""};
×
782
        }
783
        String[] res = new String[3];
×
784
        res[0] = getPath().substring(0, getPath().lastIndexOf(PATH_SEPARATOR) + 1);
×
785
        res[1] = res[0];
×
786
        res[2] = "";
×
787
        return res;
×
788
    }
789

790
    /**
791
     * Get the project {@link #getPath()} refers to.
792
     *
793
     * @return {@code null} if not available, the project otherwise.
794
     */
795
    @Nullable
796
    public Project getProject() {
797
        return Project.getProject(getResourceFile());
1✔
798
    }
799

800
    /**
801
     * Same as {@link #getRequestedProjects()} but returns the project names as
802
     * a coma separated String.
803
     *
804
     * @return a possible empty String but never {@code null}.
805
     */
806
    public String getRequestedProjectsAsString() {
807
        if (requestedProjectsString == null) {
×
808
            requestedProjectsString = String.join(",", getRequestedProjects());
×
809
        }
810
        return requestedProjectsString;
×
811
    }
812

813
    /**
814
     * Get a reference to a set of requested projects via request parameter
815
     * {@code project} or cookies or defaults.
816
     * <p>
817
     * NOTE: This method assumes, that project names do <b>not</b> contain a
818
     * comma (','), since this character is used as name separator!
819
     * <p>
820
     * It is determined as follows:
821
     * <ol>
822
     * <li>If there is no project in the configuration an empty set is returned. Otherwise:</li>
823
     * <li>If there is only one project in the configuration,
824
     * this one gets returned (no matter, what the request actually says). Otherwise</li>
825
     * <li>If the request parameter {@code ALL_PROJECT_SEARCH} contains a true value,
826
     * all projects are added to searching. Otherwise:</li>
827
     * <li>If the request parameter {@code PROJECT_PARAM_NAME} contains any available project,
828
     * the set with invalid projects removed gets returned. Otherwise:</li>
829
     * <li>If the request parameter {@code GROUP_PARAM_NAME} contains any available group,
830
     * then all projects from that group will be added to the result set. Otherwise:</li>
831
     * <li>If the request has a cookie with the name {@code OPEN_GROK_PROJECT}
832
     * and it contains any available project,
833
     * the set with invalid projects removed gets returned. Otherwise:</li>
834
     * <li>If a default project is set in the configuration,
835
     * this project gets returned. Otherwise:</li>
836
     * <li>an empty set</li>
837
     * </ol>
838
     *
839
     * @return a possible empty set of project names but never {@code null}.
840
     * @see QueryParameters#ALL_PROJECT_SEARCH
841
     * @see #PROJECT_PARAM_NAME
842
     * @see #GROUP_PARAM_NAME
843
     * @see #OPEN_GROK_PROJECT
844
     */
845
    public SortedSet<String> getRequestedProjects() {
846
        if (requestedProjects == null) {
1✔
847
            requestedProjects = getRequestedProjects(
1✔
848
                    QueryParameters.ALL_PROJECT_SEARCH, PROJECT_PARAM_NAME, GROUP_PARAM_NAME, OPEN_GROK_PROJECT);
849
        }
850
        return requestedProjects;
1✔
851
    }
852

853
    private static final Pattern COMMA_PATTERN = Pattern.compile(",");
1✔
854

855
    private static void splitByComma(String value, List<String> result) {
856
        if (value == null || value.length() == 0) {
1✔
857
            return;
×
858
        }
859
        String[] p = COMMA_PATTERN.split(value);
1✔
860
        for (String p1 : p) {
1✔
861
            if (p1.length() != 0) {
1✔
862
                result.add(p1);
1✔
863
            }
864
        }
865
    }
1✔
866

867
    /**
868
     * Get the cookie values for the given name. Splits comma separated values
869
     * automatically into a list of Strings.
870
     *
871
     * @param cookieName name of the cookie.
872
     * @return a possible empty list.
873
     */
874
    public List<String> getCookieVals(String cookieName) {
875
        Cookie[] cookies = req.getCookies();
1✔
876
        ArrayList<String> res = new ArrayList<>();
1✔
877
        if (cookies != null) {
1✔
878
            for (int i = cookies.length - 1; i >= 0; i--) {
1✔
879
                if (cookies[i].getName().equals(cookieName)) {
1✔
880
                    String value = URLDecoder.decode(cookies[i].getValue(), StandardCharsets.UTF_8);
1✔
881
                    splitByComma(value, res);
1✔
882
                }
883
            }
884
        }
885
        return res;
1✔
886
    }
887

888
    /**
889
     * Get the parameter values for the given name. Splits comma separated
890
     * values automatically into a list of Strings.
891
     *
892
     * @param paramName name of the parameter.
893
     * @return a possible empty list.
894
     */
895
    private List<String> getParameterValues(String paramName) {
896
        String[] parameterValues = req.getParameterValues(paramName);
1✔
897
        List<String> res = new ArrayList<>();
1✔
898
        if (parameterValues != null) {
1✔
899
            for (int i = parameterValues.length - 1; i >= 0; i--) {
1✔
900
                splitByComma(parameterValues[i], res);
1✔
901
            }
902
        }
903
        return res;
1✔
904
    }
905

906
    /**
907
     * Same as {@link #getRequestedProjects()}, but with a variable cookieName
908
     * and parameter name.
909
     *
910
     * @param searchAllParamName the name of the request parameter corresponding to search all projects.
911
     * @param projectParamName   the name of the request parameter corresponding to a project name.
912
     * @param groupParamName     the name of the request parameter corresponding to a group name
913
     * @param cookieName         name of the cookie which possible contains project
914
     *                           names used as fallback
915
     * @return set of project names. Possibly empty set but never {@code null}.
916
     */
917
    protected SortedSet<String> getRequestedProjects(
918
            String searchAllParamName,
919
            String projectParamName,
920
            String groupParamName,
921
            String cookieName
922
    ) {
923

924
        TreeSet<String> projectNames = new TreeSet<>();
1✔
925
        List<Project> projects = getEnv().getProjectList();
1✔
926

927
        if (Boolean.parseBoolean(req.getParameter(searchAllParamName))) {
1✔
928
            return getProjectHelper()
1✔
929
                    .getAllProjects()
1✔
930
                    .stream()
1✔
931
                    .map(Project::getName)
1✔
932
                    .collect(Collectors.toCollection(TreeSet::new));
1✔
933
        }
934

935
        // Use a project determined directly from the URL
936
        if (getProject() != null && getProject().isIndexed()) {
1✔
937
            projectNames.add(getProject().getName());
×
938
            return projectNames;
×
939
        }
940

941
        // Use a project if there is just a single project.
942
        if (projects.size() == 1) {
1✔
943
            Project p = projects.get(0);
×
944
            if (p.isIndexed() && authFramework.isAllowed(req, p)) {
×
945
                projectNames.add(p.getName());
×
946
            }
947
            return projectNames;
×
948
        }
949

950
        // Add all projects which match the project parameter name values/
951
        List<String> names = getParameterValues(projectParamName);
1✔
952
        for (String projectName : names) {
1✔
953
            Project project = Project.getByName(projectName);
1✔
954
            if (project != null && project.isIndexed() && authFramework.isAllowed(req, project)) {
1✔
955
                projectNames.add(projectName);
1✔
956
            }
957
        }
1✔
958

959
        // Add all projects which are part of a group that matches the group parameter name.
960
        names = getParameterValues(groupParamName);
1✔
961
        for (String groupName : names) {
1✔
962
            Group group = Group.getByName(groupName);
1✔
963
            if (group != null) {
1✔
964
                projectNames.addAll(getProjectHelper().getAllGrouped(group)
1✔
965
                                                      .stream()
1✔
966
                                                      .filter(Project::isIndexed)
1✔
967
                                                      .map(Project::getName)
1✔
968
                                                      .collect(Collectors.toSet()));
1✔
969
            }
970
        }
1✔
971

972
        // Add projects based on cookie.
973
        if (projectNames.isEmpty() && getIntParam(QueryParameters.NUM_SELECTED_PARAM, -1) != 0) {
1✔
974
            List<String> cookies = getCookieVals(cookieName);
1✔
975
            for (String s : cookies) {
1✔
976
                Project x = Project.getByName(s);
×
977
                if (x != null && x.isIndexed() && authFramework.isAllowed(req, x)) {
×
978
                    projectNames.add(s);
×
979
                }
980
            }
×
981
        }
982

983
        // Add default projects.
984
        if (projectNames.isEmpty()) {
1✔
985
            Set<Project> defaultProjects = env.getDefaultProjects();
1✔
986
            if (defaultProjects != null) {
1✔
987
                for (Project project : defaultProjects) {
×
988
                    if (project.isIndexed() && authFramework.isAllowed(req, project)) {
×
989
                        projectNames.add(project.getName());
×
990
                    }
991
                }
×
992
            }
993
        }
994

995
        return projectNames;
1✔
996
    }
997

998
    public ProjectHelper getProjectHelper() {
999
        return ProjectHelper.getInstance(this);
1✔
1000
    }
1001

1002
    /**
1003
     * Set the page title to use.
1004
     *
1005
     * @param title title to set (might be {@code null}).
1006
     */
1007
    public void setTitle(String title) {
1008
        pageTitle = title;
×
1009
    }
×
1010

1011
    /**
1012
     * Get the page title to use.
1013
     *
1014
     * @return {@code null} if not set, the page title otherwise.
1015
     */
1016
    public String getTitle() {
1017
        return pageTitle;
×
1018
    }
1019

1020
    /**
1021
     * Get the base path to use to refer to CSS stylesheets and related
1022
     * resources. Usually used to create links.
1023
     *
1024
     * @return the appropriate application directory prefixed with the
1025
     * application's context path (e.g. "/source/default").
1026
     * @see HttpServletRequest#getContextPath()
1027
     * @see RuntimeEnvironment#getWebappLAF()
1028
     */
1029
    public String getCssDir() {
1030
        return req.getContextPath() + PATH_SEPARATOR + getEnv().getWebappLAF();
×
1031
    }
1032

1033
    /**
1034
     * Get the current runtime environment.
1035
     *
1036
     * @return the runtime env.
1037
     * @see RuntimeEnvironment#getInstance()
1038
     */
1039
    public RuntimeEnvironment getEnv() {
1040
        if (env == null) {
1✔
1041
            env = RuntimeEnvironment.getInstance();
1✔
1042
        }
1043
        return env;
1✔
1044
    }
1045

1046
    /**
1047
     * Get the name patterns used to determine, whether a file should be
1048
     * ignored.
1049
     *
1050
     * @return the corresponding value from the current runtime config..
1051
     */
1052
    public IgnoredNames getIgnoredNames() {
1053
        if (ignoredNames == null) {
1✔
1054
            ignoredNames = getEnv().getIgnoredNames();
1✔
1055
        }
1056
        return ignoredNames;
1✔
1057
    }
1058

1059
    /**
1060
     * Get the canonical path to root of the source tree. File separators are
1061
     * replaced with a {@link org.opengrok.indexer.index.Indexer#PATH_SEPARATOR}.
1062
     *
1063
     * @return The on disk source root directory.
1064
     * @see RuntimeEnvironment#getSourceRootPath()
1065
     */
1066
    public String getSourceRootPath() {
1067
        if (sourceRootPath == null) {
1✔
1068
            String srcPathEnv = getEnv().getSourceRootPath();
1✔
1069
            if (srcPathEnv != null) {
1✔
1070
                sourceRootPath = srcPathEnv.replace(File.separatorChar, PATH_SEPARATOR);
1✔
1071
            }
1072
        }
1073
        return sourceRootPath;
1✔
1074
    }
1075

1076
    /**
1077
     * Get the prefix for the related request.
1078
     *
1079
     * @return {@link Prefix#UNKNOWN} if the servlet path matches any known
1080
     * prefix, the prefix otherwise.
1081
     */
1082
    public Prefix getPrefix() {
1083
        if (prefix == null) {
1✔
1084
            prefix = Prefix.get(req.getServletPath());
1✔
1085
        }
1086
        return prefix;
1✔
1087
    }
1088

1089
    /**
1090
     * Get the canonical path of the related resource relative to the source
1091
     * root directory (used file separators are all {@link org.opengrok.indexer.index.Indexer#PATH_SEPARATOR}).
1092
     * No check is made, whether the obtained path is really an accessible resource on disk.
1093
     *
1094
     * @see HttpServletRequest#getPathInfo()
1095
     * @return a possible empty String (denotes the source root directory) but not {@code null}.
1096
     */
1097
    public String getPath() {
1098
        if (path == null) {
1✔
1099
            path = Util.getCanonicalPath(Laundromat.launderInput(req.getPathInfo()), PATH_SEPARATOR);
1✔
1100
            if (PATH_SEPARATOR_STRING.equals(path)) {
1✔
1101
                path = "";
1✔
1102
            }
1103
        }
1104
        return path;
1✔
1105
    }
1106

1107
    private void setPath(String path) {
1108
        this.path = Util.getCanonicalPath(Laundromat.launderInput(path), PATH_SEPARATOR);
1✔
1109
    }
1✔
1110

1111
    /**
1112
     * @return true if file/directory corresponding to the request path exists however is unreadable, false otherwise
1113
     */
1114
    public boolean isUnreadable() {
1115
        File file = new File(getSourceRootPath(), getPath());
×
1116
        return file.exists() && !file.canRead();
×
1117
    }
1118

1119
    /**
1120
     * Get the file object for the given path.
1121
     * <p>
1122
     * NOTE: If a repository contains hard or symbolic links, the returned file
1123
     * may finally point to a file outside the source root directory.
1124
     * </p>
1125
     * @param path the path to the file relatively to the source root
1126
     * @return null if the related file or directory is not
1127
     * available (can not be found below the source root directory), the readable file or directory otherwise.
1128
     * @see #getSourceRootPath()
1129
     */
1130
    public File getResourceFile(String path) {
1131
        File file;
1132
        file = new File(getSourceRootPath(), path);
1✔
1133
        if (!file.canRead()) {
1✔
1134
            return null;
1✔
1135
        }
1136
        return file;
1✔
1137
    }
1138

1139
    /**
1140
     * Get the on disk file to the request related file or directory.
1141
     * <p>
1142
     * NOTE: If a repository contains hard or symbolic links, the returned file
1143
     * may finally point to a file outside the source root directory.
1144
     * </p>
1145
     * @return {@code new File({@link org.opengrok.indexer.index.Indexer#PATH_SEPARATOR_STRING })}
1146
     * if the related file or directory is not available (can not be find below the source root directory),
1147
     * the readable file or directory otherwise.
1148
     * @see #getSourceRootPath()
1149
     * @see #getPath()
1150
     */
1151
    public File getResourceFile() {
1152
        if (resourceFile == null) {
1✔
1153
            resourceFile = getResourceFile(getPath());
1✔
1154
            if (resourceFile == null) {
1✔
1155
                resourceFile = new File(PATH_SEPARATOR_STRING);
1✔
1156
            }
1157
        }
1158
        return resourceFile;
1✔
1159
    }
1160

1161
    /**
1162
     * Get the canonical on disk path to the request related file or directory
1163
     * with all file separators replaced by a {@link org.opengrok.indexer.index.Indexer#PATH_SEPARATOR}.
1164
     *
1165
     * @return {@link org.opengrok.indexer.index.Indexer#PATH_SEPARATOR_STRING} if the evaluated path is invalid
1166
     * or outside the source root directory, otherwise the path to the readable file or directory.
1167
     * @see #getResourceFile()
1168
     */
1169
    public String getResourcePath() {
1170
        if (resourcePath == null) {
1✔
1171
            resourcePath = Util.fixPathIfWindows(getResourceFile().getPath());
1✔
1172
        }
1173
        return resourcePath;
1✔
1174
    }
1175

1176
    /**
1177
     * Check, whether the related request resource matches a valid file or
1178
     * directory below the source root directory and whether it matches an
1179
     * ignored pattern.
1180
     *
1181
     * @return {@code true} if the related resource does not exist or should be ignored.
1182
     * @see #getIgnoredNames()
1183
     * @see #getResourcePath()
1184
     */
1185
    public boolean resourceNotAvailable() {
1186
        getIgnoredNames();
1✔
1187
        return getResourcePath().equals(PATH_SEPARATOR_STRING) || ignoredNames.ignore(getPath())
1✔
1188
                || ignoredNames.ignore(resourceFile.getParentFile())
1✔
1189
                || ignoredNames.ignore(resourceFile);
1✔
1190
    }
1191

1192
    /**
1193
     * Check, whether the request related path represents a directory.
1194
     *
1195
     * @return {@code true} if directory related request
1196
     */
1197
    public boolean isDir() {
1198
        if (isDir == null) {
1✔
1199
            isDir = getResourceFile().isDirectory();
1✔
1200
        }
1201
        return isDir;
1✔
1202
    }
1203

1204
    private static String trailingSlash(String path) {
1205
        return path.length() == 0 || path.charAt(path.length() - 1) != PATH_SEPARATOR
1✔
1206
                ? PATH_SEPARATOR_STRING
1✔
1207
                : "";
1✔
1208
    }
1209

1210
    @Nullable
1211
    private File checkFileInner(File file, File dir, String name) {
1212

1213
        MeterRegistry meterRegistry = Metrics.getRegistry();
×
1214

1215
        File xrefFile = new File(dir, name);
×
1216
        if (xrefFile.exists() && xrefFile.isFile()) {
×
1217
            if (xrefFile.lastModified() >= file.lastModified()) {
×
1218
                if (meterRegistry != null) {
×
1219
                    Counter.builder("xref.file.get").
×
1220
                            description("xref file get hits").
×
1221
                            tag("what", "hits").
×
1222
                            register(meterRegistry).
×
1223
                            increment();
×
1224
                }
1225
                return xrefFile;
×
1226
            } else {
1227
                LOGGER.log(Level.WARNING, "file ''{0}'' is newer than ''{1}''", new Object[]{file, xrefFile});
×
1228
            }
1229
        }
1230

1231
        if (meterRegistry != null) {
×
1232
            Counter.builder("xref.file.get").
×
1233
                    description("xref file get misses").
×
1234
                    tag("what", "miss").
×
1235
                    register(meterRegistry).
×
1236
                    increment();
×
1237
        }
1238

1239
        return null;
×
1240
    }
1241

1242
    @Nullable
1243
    private File checkFile(File file, File dir, String name, boolean compressed) {
1244
        File f;
1245
        if (compressed) {
×
1246
            f = checkFileInner(file, dir, TandemPath.join(name, ".gz"));
×
1247
            if (f != null) {
×
1248
                return f;
×
1249
            }
1250
        }
1251

1252
        return checkFileInner(file, dir, name);
×
1253
    }
1254

1255
    @Nullable
1256
    private File checkFileResolve(File dir, String name, boolean compressed) {
1257
        File lresourceFile = new File(getSourceRootPath() + getPath(), name);
×
1258
        if (!lresourceFile.canRead()) {
×
1259
            lresourceFile = new File(PATH_SEPARATOR_STRING);
×
1260
        }
1261

1262
        return checkFile(lresourceFile, dir, name, compressed);
×
1263
    }
1264

1265
    /**
1266
     * Find the files with the given names in the {@link #getPath()} directory
1267
     * relative to the cross-file directory of the opengrok data directory. It will
1268
     * try to find the compressed file first by appending the file extension
1269
     * {@code ".gz"} to the filename. If that fails or an uncompressed version of the
1270
     * file is younger than its compressed version, the uncompressed file gets used.
1271
     *
1272
     * @param filenames filenames to lookup.
1273
     * @return an empty array if the related directory does not exist or the
1274
     * given list is {@code null} or empty, otherwise an array, which may
1275
     * contain {@code null} entries (when the related file could not be found)
1276
     * having the same order as the given list.
1277
     */
1278
    public File[] findDataFiles(@Nullable List<String> filenames) {
1279
        if (filenames == null || filenames.isEmpty()) {
×
1280
            return new File[0];
×
1281
        }
1282
        File[] res = new File[filenames.size()];
×
1283
        File dir = new File(getEnv().getDataRootPath() + Prefix.XREF_P + getPath());
×
1284
        if (dir.exists() && dir.isDirectory()) {
×
1285
            getResourceFile();
×
1286
            boolean compressed = getEnv().isCompressXref();
×
1287
            for (int i = res.length - 1; i >= 0; i--) {
×
1288
                res[i] = checkFileResolve(dir, filenames.get(i), compressed);
×
1289
            }
1290
        }
1291
        return res;
×
1292
    }
1293

1294
    /**
1295
     * Lookup the file {@link #getPath()} relative to the cross-file directory of
1296
     * the opengrok data directory. It is tried to find the compressed file
1297
     * first by appending the file extension ".gz" to the filename. If that
1298
     * fails or an uncompressed version of the file is younger than its
1299
     * compressed version, the uncompressed file gets used.
1300
     *
1301
     * @return {@code null} if not found, the file otherwise.
1302
     */
1303
    public File findDataFile() {
1304
        return checkFile(resourceFile, new File(getEnv().getDataRootPath() + Prefix.XREF_P),
×
1305
                getPath(), env.isCompressXref());
×
1306
    }
1307

1308
    /**
1309
     * Is revision the latest revision ?
1310
     * @param rev revision string
1311
     * @return true if latest revision, false otherwise
1312
     */
1313
    public boolean isLatestRevision(String rev) {
1314
        return rev.equals(getLatestRevision(getResourceFile()));
×
1315
    }
1316

1317
    /**
1318
     * Get the location of cross-reference for given file containing the given revision.
1319
     * @param revStr defined revision string
1320
     * @return location to redirect to
1321
     */
1322
    public String getRevisionLocation(String revStr) {
1323
        StringBuilder sb = new StringBuilder();
1✔
1324

1325
        sb.append(req.getContextPath());
1✔
1326
        sb.append(Prefix.XREF_P);
1✔
1327
        sb.append(Util.uriEncodePath(getPath()));
1✔
1328
        sb.append("?");
1✔
1329
        sb.append(QueryParameters.REVISION_PARAM_EQ);
1✔
1330
        sb.append(Util.uriEncode(revStr));
1✔
1331

1332
        if (req.getQueryString() != null) {
1✔
1333
            sb.append("&");
1✔
1334
            sb.append(req.getQueryString());
1✔
1335
        }
1336
        if (fragmentIdentifier != null) {
1✔
1337
            String anchor = Util.uriEncode(fragmentIdentifier);
×
1338

1339
            String reqFrag = req.getParameter(QueryParameters.FRAGMENT_IDENTIFIER_PARAM);
×
1340
            if (reqFrag == null || reqFrag.isEmpty()) {
×
1341
                /*
1342
                 * We've determined that the fragmentIdentifier field must have
1343
                 * been set to augment request parameters. Now include it
1344
                 * explicitly in the next request parameters.
1345
                 */
1346
                sb.append("&");
×
1347
                sb.append(QueryParameters.FRAGMENT_IDENTIFIER_PARAM_EQ);
×
1348
                sb.append(anchor);
×
1349
            }
1350
            sb.append("#");
×
1351
            sb.append(anchor);
×
1352
        }
1353

1354
        return sb.toString();
1✔
1355
    }
1356

1357
    /**
1358
     * Get the path the request should be redirected (if any).
1359
     *
1360
     * @return {@code null} if there is no reason to redirect, the URI encoded
1361
     * redirect path to use otherwise.
1362
     */
1363
    public String getDirectoryRedirect() {
1364
        if (isDir()) {
1✔
1365
            getPrefix();
1✔
1366
            // Redirect /xref -> /xref/
1367
            if (prefix == Prefix.XREF_P
1✔
1368
                    && getUriEncodedPath().isEmpty()
1✔
1369
                    && !req.getRequestURI().endsWith("/")) {
×
1370
                return req.getContextPath() + Prefix.XREF_P + '/';
×
1371
            }
1372

1373
            if (getPath().length() == 0) {
1✔
1374
                // => /
1375
                return null;
×
1376
            }
1377

1378
            if (prefix != Prefix.XREF_P && prefix != Prefix.HIST_L
1✔
1379
                    && prefix != Prefix.RSS_P) {
1380
                // if it is an existing dir perhaps people wanted dir xref
1381
                return req.getContextPath() + Prefix.XREF_P
×
1382
                        + getUriEncodedPath() + trailingSlash(getPath());
×
1383
            }
1384
            String ts = trailingSlash(getPath());
1✔
1385
            if (ts.length() != 0) {
1✔
1386
                return req.getContextPath() + prefix + getUriEncodedPath() + ts;
1✔
1387
            }
1388
        }
1389
        return null;
1✔
1390
    }
1391

1392
    /**
1393
     * Get the URI encoded canonical path to the related file or directory (the
1394
     * URI part between the servlet path and the start of the query string).
1395
     *
1396
     * @return a URI encoded path which might be an empty string but not {@code null}.
1397
     * @see #getPath()
1398
     */
1399
    public String getUriEncodedPath() {
1400
        if (uriEncodedPath == null) {
1✔
1401
            uriEncodedPath = Util.uriEncodePath(getPath());
1✔
1402
        }
1403
        return uriEncodedPath;
1✔
1404
    }
1405

1406
    /**
1407
     * Add a new file script to the page by the name.
1408
     *
1409
     * @param name name of the script to search for
1410
     * @return this
1411
     *
1412
     * @see Scripts#addScript(String, String, Scripts.Type)
1413
     */
1414
    public PageConfig addScript(String name) {
1415
        this.scripts.addScript(this.req.getContextPath(), name, isDebug() ? Scripts.Type.DEBUG : Scripts.Type.MINIFIED);
×
1416
        return this;
×
1417
    }
1418

1419
    private boolean isDebug() {
1420
        return Boolean.parseBoolean(req.getParameter(DEBUG_PARAM_NAME));
×
1421
    }
1422

1423
    /**
1424
     * Return the page scripts.
1425
     *
1426
     * @return the scripts
1427
     *
1428
     * @see Scripts
1429
     */
1430
    public Scripts getScripts() {
1431
        return this.scripts;
×
1432
    }
1433

1434
    /**
1435
     * Get opengrok's configured data root directory. It is verified, that the
1436
     * used environment has a valid opengrok data root set and that it is an
1437
     * accessible directory.
1438
     *
1439
     * @return the opengrok data directory.
1440
     * @throws InvalidParameterException if inaccessible or not set.
1441
     */
1442
    public File getDataRoot() {
1443
        if (dataRoot == null) {
1✔
1444
            String tmp = getEnv().getDataRootPath();
1✔
1445
            if (tmp == null || tmp.length() == 0) {
1✔
1446
                throw new InvalidParameterException("dataRoot parameter is not "
×
1447
                        + "set in configuration.xml!");
1448
            }
1449
            dataRoot = new File(tmp);
1✔
1450
            if (!(dataRoot.isDirectory() && dataRoot.canRead())) {
1✔
1451
                throw new InvalidParameterException("The configured dataRoot '"
×
1452
                        + tmp
1453
                        + "' refers to a none-existing or unreadable directory!");
1454
            }
1455
        }
1456
        return dataRoot;
1✔
1457
    }
1458

1459
    /**
1460
     * Play nice in reverse proxy environment by using pre-configured hostname request to construct the URLs.
1461
     * Will not work well if the scheme or port is different for proxied server and original server.
1462
     * @return server name
1463
     */
1464
    public String getServerName() {
1465
        if (env.getServerName() != null) {
×
1466
            return env.getServerName();
×
1467
        } else {
1468
            return req.getServerName();
×
1469
        }
1470
    }
1471

1472
    /**
1473
     * Prepare a search helper with all required information, ready to execute
1474
     * the query implied by the related request parameters and cookies.
1475
     * <p>
1476
     * NOTE: One should check the {@link SearchHelper#getErrorMsg()} as well as
1477
     * {@link SearchHelper#getRedirect()} and take the appropriate action before
1478
     * executing the prepared query or continue processing.
1479
     * <p>
1480
     * This method stops populating fields as soon as an error occurs.
1481
     *
1482
     * @return a search helper.
1483
     */
1484
    public SearchHelper prepareSearch() {
1485
        List<SortOrder> sortOrders = getSortOrder();
×
1486
        SearchHelper sh = prepareInternalSearch(sortOrders.isEmpty() ? SortOrder.RELEVANCY : sortOrders.get(0));
×
1487

1488
        if (getRequestedProjects().isEmpty() && getEnv().hasProjects()) {
×
1489
            sh.setErrorMsg("You must select a project!");
×
1490
            return sh;
×
1491
        }
1492

1493
        if (sh.getBuilder().getSize() == 0) {
×
1494
            // Entry page show the map
1495
            sh.setRedirect(req.getContextPath() + '/');
×
1496
            return sh;
×
1497
        }
1498

1499
        return sh;
×
1500
    }
1501

1502
    /**
1503
     * Prepare a search helper with required settings for an internal search.
1504
     * <p>
1505
     * NOTE: One should check the {@link SearchHelper#getErrorMsg()} as well as
1506
     * {@link SearchHelper#getRedirect()} and take the appropriate action before
1507
     * executing the prepared query or continue processing.
1508
     * <p>
1509
     * This method stops populating fields as soon as an error occurs.
1510
     * @param sortOrder instance of {@link SortOrder}
1511
     * @return a search helper.
1512
     */
1513
    public SearchHelper prepareInternalSearch(SortOrder sortOrder) {
1514
        String xrValue = req.getParameter(QueryParameters.NO_REDIRECT_PARAM);
1✔
1515
        return new SearchHelper(getStartIndex(), sortOrder, getDataRoot(), new File(getSourceRootPath()),
1✔
1516
                getMaxItems(), getEftarReader(), getQueryBuilder(), getPrefix() == Prefix.SEARCH_R,
1✔
1517
                req.getContextPath(), getPrefix() == Prefix.SEARCH_R || getPrefix() == Prefix.SEARCH_P,
1✔
1518
                xrValue != null && !xrValue.isEmpty());
1✔
1519
    }
1520

1521
    /**
1522
     * @param project {@link Project} instance or {@code null}
1523
     * @param request HTTP request
1524
     * @return list of {@link NullableNumLinesLOC} instances related to {@link #path}
1525
     * @throws IOException on I/O error
1526
     */
1527
    @Nullable
1528
    public List<NullableNumLinesLOC> getExtras(@Nullable Project project, HttpServletRequest request)
1529
            throws IOException {
1530

1531
        SearchHelper searchHelper = prepareInternalSearch(SortOrder.RELEVANCY);
1✔
1532
        /*
1533
         * N.b. searchHelper.destroy() is called via
1534
         * WebappListener.requestDestroyed() on presence of the following
1535
         * REQUEST_ATTR.
1536
         */
1537
        request.setAttribute(SearchHelper.REQUEST_ATTR, searchHelper);
1✔
1538
        if (project != null) {
1✔
1539
            searchHelper.prepareExec(project);
1✔
1540
        } else {
1541
            //noinspection Convert2Diamond
1542
            searchHelper.prepareExec(new TreeSet<String>());
×
1543
        }
1544

1545
        return getNullableNumLinesLOCS(project, searchHelper);
1✔
1546
    }
1547

1548
    @Nullable
1549
    @VisibleForTesting
1550
    public List<NullableNumLinesLOC> getNullableNumLinesLOCS(@Nullable Project project, SearchHelper searchHelper)
1551
            throws IOException {
1552

1553
        if (searchHelper.getSearcher() != null) {
1✔
1554
            DirectoryExtraReader extraReader = new DirectoryExtraReader();
1✔
1555
            String primePath = path;
1✔
1556
            try {
1557
                primePath = searchHelper.getPrimeRelativePath(project != null ? project.getName() : "", path);
1✔
1558
            } catch (IOException | ForbiddenSymlinkException ex) {
×
1559
                LOGGER.log(Level.WARNING, String.format(
×
1560
                        "Error getting prime relative for %s", path), ex);
1561
            }
1✔
1562
            return extraReader.search(searchHelper.getSearcher(), primePath);
1✔
1563
        }
1564

1565
        return null;
×
1566
    }
1567

1568
    /**
1569
     * Get the config w.r.t. the given request. If there is none yet, a new config
1570
     * gets created, attached to the request and returned.
1571
     * <p>
1572
     *
1573
     * @param request the request to use to initialize the config parameters.
1574
     * @return always the same none-{@code null} config for a given request.
1575
     * @throws NullPointerException if the given parameter is {@code null}.
1576
     */
1577
    public static PageConfig get(HttpServletRequest request) {
1578
        Object cfg = request.getAttribute(ATTR_NAME);
1✔
1579
        if (cfg != null) {
1✔
1580
            return (PageConfig) cfg;
×
1581
        }
1582
        PageConfig pcfg = new PageConfig(request);
1✔
1583
        request.setAttribute(ATTR_NAME, pcfg);
1✔
1584
        return pcfg;
1✔
1585
    }
1586

1587
    public static PageConfig get(String path, HttpServletRequest request) {
1588
        PageConfig pcfg = new PageConfig(request);
1✔
1589
        pcfg.setPath(path);
1✔
1590
        return pcfg;
1✔
1591
    }
1592

1593
    private PageConfig() {
1✔
1594
        this.authFramework = RuntimeEnvironment.getInstance().getAuthorizationFramework();
1✔
1595
        this.executor = RuntimeEnvironment.getInstance().getRevisionExecutor();
1✔
1596
    }
1✔
1597

1598
    private PageConfig(HttpServletRequest req) {
1599
        this();
1✔
1600

1601
        this.req = req;
1✔
1602
        this.fragmentIdentifier = req.getParameter(QueryParameters.FRAGMENT_IDENTIFIER_PARAM);
1✔
1603
    }
1✔
1604

1605
    /**
1606
     * Cleanup all allocated resources (if any) from the instance attached to
1607
     * the given request.
1608
     *
1609
     * @param sr request to check, cleanup. Ignored if {@code null}.
1610
     * @see PageConfig#get(HttpServletRequest)
1611
     */
1612
    public static void cleanup(ServletRequest sr) {
1613
        if (sr == null) {
1✔
1614
            return;
×
1615
        }
1616
        PageConfig cfg = (PageConfig) sr.getAttribute(ATTR_NAME);
1✔
1617
        if (cfg == null) {
1✔
1618
            return;
1✔
1619
        }
1620
        ProjectHelper.cleanup(cfg);
1✔
1621
        sr.removeAttribute(ATTR_NAME);
1✔
1622
        cfg.env = null;
1✔
1623
        cfg.req = null;
1✔
1624
        if (cfg.eftarReader != null) {
1✔
1625
            cfg.eftarReader.close();
×
1626
        }
1627
    }
1✔
1628

1629
    /**
1630
     * Checks if current request is allowed to access project.
1631
     * @param t project
1632
     * @return true if yes
1633
     */
1634
    public boolean isAllowed(Project t) {
1635
        return this.authFramework.isAllowed(this.req, t);
1✔
1636
    }
1637

1638
    /**
1639
     * Checks if current request is allowed to access group.
1640
     * @param g group
1641
     * @return true if yes
1642
     */
1643
    public boolean isAllowed(Group g) {
1644
        return this.authFramework.isAllowed(this.req, g);
1✔
1645
    }
1646

1647

1648
    public SortedSet<AcceptedMessage> getMessages() {
1649
        return env.getMessages();
×
1650
    }
1651

1652
    public SortedSet<AcceptedMessage> getMessages(String tag) {
1653
        return env.getMessages(tag);
×
1654
    }
1655

1656
    /**
1657
     * Get basename of the path or "/" if the path is empty.
1658
     * This is used for setting title of various pages.
1659
     * @param path path
1660
     * @return short version of the path
1661
     */
1662
    public String getShortPath(String path) {
1663
        File file = new File(path);
×
1664

1665
        if (path.isEmpty()) {
×
1666
            return "/";
×
1667
        }
1668

1669
        return file.getName();
×
1670
    }
1671

1672
    private String addTitleDelimiter(String title) {
1673
        if (!title.isEmpty()) {
×
1674
            return title + ", ";
×
1675
        }
1676

1677
        return title;
×
1678
    }
1679

1680
    /**
1681
     * The search page title string should progressively reflect the search terms
1682
     * so that if only small portion of the string is seen, it describes
1683
     * the action as closely as possible while remaining readable.
1684
     * @return string used for setting page title of search results page
1685
     */
1686
    public String getSearchTitle() {
1687
        String title = "";
×
1688

1689
        if (req.getParameter(QueryBuilder.FULL) != null && !req.getParameter(QueryBuilder.FULL).isEmpty()) {
×
1690
            title += req.getParameter(QueryBuilder.FULL) + " (full)";
×
1691
        }
1692
        if (req.getParameter(QueryBuilder.DEFS) != null && !req.getParameter(QueryBuilder.DEFS).isEmpty()) {
×
1693
            title = addTitleDelimiter(title);
×
1694
            title += req.getParameter(QueryBuilder.DEFS) + " (definition)";
×
1695
        }
1696
        if (req.getParameter(QueryBuilder.REFS) != null && !req.getParameter(QueryBuilder.REFS).isEmpty()) {
×
1697
            title = addTitleDelimiter(title);
×
1698
            title += req.getParameter(QueryBuilder.REFS) + " (reference)";
×
1699
        }
1700
        if (req.getParameter(QueryBuilder.PATH) != null && !req.getParameter(QueryBuilder.PATH).isEmpty()) {
×
1701
            title = addTitleDelimiter(title);
×
1702
            title += req.getParameter(QueryBuilder.PATH) + " (path)";
×
1703
        }
1704
        if (req.getParameter(QueryBuilder.HIST) != null && !req.getParameter(QueryBuilder.HIST).isEmpty()) {
×
1705
            title = addTitleDelimiter(title);
×
1706
            title += req.getParameter(QueryBuilder.HIST) + " (history)";
×
1707
        }
1708

1709
        if (req.getParameterValues(QueryBuilder.PROJECT) != null && req.getParameterValues(QueryBuilder.PROJECT).length != 0) {
×
1710
            if (!title.isEmpty()) {
×
1711
                title += " ";
×
1712
            }
1713
            title += "in projects: ";
×
1714
            String[] projects = req.getParameterValues(QueryBuilder.PROJECT);
×
1715
            title += String.join(",", projects);
×
1716
        }
1717

1718
        return Util.htmlize(title + " - OpenGrok search results");
×
1719
    }
1720

1721
    /**
1722
     * Similar as {@link #getSearchTitle()}.
1723
     * @return string used for setting page title of search view
1724
     */
1725
    public String getHistoryTitle() {
1726
        String path = getPath();
×
1727
        return Util.htmlize(getShortPath(path) + " - OpenGrok history log for " + path);
×
1728
    }
1729

1730
    public String getPathTitle() {
1731
        String path = getPath();
×
1732
        String title = getShortPath(path);
×
1733
        if (!getRequestedRevision().isEmpty()) {
×
1734
            title += " (revision " + getRequestedRevision() + ")";
×
1735
        }
1736
        title += " - OpenGrok cross reference for " + (path.isEmpty() ? "/" : path);
×
1737

1738
        return Util.htmlize(title);
×
1739
    }
1740

1741
    public void checkSourceRootExistence() throws IOException {
1742
        if (getSourceRootPath() == null || getSourceRootPath().isEmpty()) {
1✔
1743
            throw new FileNotFoundException("Unable to determine source root path. Missing configuration?");
1✔
1744
        }
1745
        File sourceRootPathFile = RuntimeEnvironment.getInstance().getSourceRootFile();
1✔
1746
        if (!sourceRootPathFile.exists()) {
1✔
1747
            throw new FileNotFoundException(String.format("Source root path \"%s\" does not exist",
1✔
1748
                    sourceRootPathFile.getAbsolutePath()));
1✔
1749
        }
1750
        if (!sourceRootPathFile.isDirectory()) {
1✔
1751
            throw new FileNotFoundException(String.format("Source root path \"%s\" is not a directory",
×
1752
                    sourceRootPathFile.getAbsolutePath()));
×
1753
        }
1754
        if (!sourceRootPathFile.canRead()) {
1✔
1755
            throw new IOException(String.format("Source root path \"%s\" is not readable",
×
1756
                    sourceRootPathFile.getAbsolutePath()));
×
1757
        }
1758
    }
1✔
1759

1760
    /**
1761
     * Get all project related messages. These include
1762
     * <ol>
1763
     * <li>Main messages</li>
1764
     * <li>Messages with tag = project name</li>
1765
     * <li>Messages with tag = project's groups names</li>
1766
     * </ol>
1767
     *
1768
     * @return the sorted set of messages according to accept time
1769
     * @see org.opengrok.indexer.web.messages.MessagesContainer#MESSAGES_MAIN_PAGE_TAG
1770
     */
1771
    private SortedSet<AcceptedMessage> getProjectMessages() {
1772
        SortedSet<AcceptedMessage> messages = getMessages();
×
1773

1774
        if (getProject() != null) {
×
1775
            messages.addAll(getMessages(getProject().getName()));
×
1776
            getProject().getGroups().forEach(group -> {
×
1777
                messages.addAll(getMessages(group.getName()));
×
1778
            });
×
1779
        }
1780

1781
        return messages;
×
1782
    }
1783

1784
    /**
1785
     * Decide if this resource has been modified since the header value in the request.
1786
     * <p>
1787
     * The resource is modified since the weak ETag value in the request, the ETag is
1788
     * computed using:
1789
     * </p>
1790
     * <ul>
1791
     * <li>the source file modification</li>
1792
     * <li>project messages</li>
1793
     * <li>last timestamp for index</li>
1794
     * <li>OpenGrok current deployed version</li>
1795
     * </ul>
1796
     *
1797
     * <p>
1798
     * If the resource was modified, appropriate headers in the response are filled.
1799
     * </p>
1800
     *
1801
     * @param request the http request containing the headers
1802
     * @param response the http response for setting the headers
1803
     * @return true if resource was not modified; false otherwise
1804
     * @see <a href="https://tools.ietf.org/html/rfc7232#section-2.3">HTTP ETag</a>
1805
     * @see <a href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html">HTTP Caching</a>
1806
     */
1807
    public boolean isNotModified(HttpServletRequest request, HttpServletResponse response) {
1808
        String currentEtag = String.format("W/\"%s\"",
×
1809
                Objects.hash(
×
1810
                        // last modified time as UTC timestamp in millis
1811
                        getLastModified(),
×
1812
                        // all project related messages which changes the view
1813
                        getProjectMessages(),
×
1814
                        // last timestamp value
1815
                        getEnv().getDateForLastIndexRun() != null ? getEnv().getDateForLastIndexRun().getTime() : 0,
×
1816
                        // OpenGrok version has changed since the last time
1817
                        Info.getVersion()
×
1818
                )
1819
        );
1820

1821
        String headerEtag = request.getHeader(HttpHeaders.IF_NONE_MATCH);
×
1822

1823
        if (headerEtag != null && headerEtag.equals(currentEtag)) {
×
1824
            // weak ETag has not changed, return 304 NOT MODIFIED
1825
            response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
×
1826
            return true;
×
1827
        }
1828

1829
        // return 200 OK
1830
        response.setHeader(HttpHeaders.ETAG, currentEtag);
×
1831
        return false;
×
1832
    }
1833

1834
    /**
1835
     * @param root root path
1836
     * @param path path
1837
     * @return path relative to root
1838
     */
1839
    public static String getRelativePath(String root, String path) {
1840
        return Paths.get(root).relativize(Paths.get(path)).toString();
×
1841
    }
1842

1843
    /**
1844
     * Determines whether a match offset from a search result has been indicated,
1845
     * and if so tries to calculate a translated xref fragment identifier.
1846
     * @return {@code true} if a xref fragment identifier was calculated by the call to this method
1847
     */
1848
    public boolean evaluateMatchOffset() {
1849
        if (fragmentIdentifier == null) {
×
1850
            int matchOffset = getIntParam(QueryParameters.MATCH_OFFSET_PARAM, -1);
×
1851
            if (matchOffset >= 0) {
×
1852
                File resourceFile = getResourceFile();
×
1853
                if (resourceFile.isFile()) {
×
1854
                    LineBreaker breaker = new LineBreaker();
×
1855
                    StreamSource streamSource = StreamSource.fromFile(resourceFile);
×
1856
                    try {
1857
                        breaker.reset(streamSource, in -> ExpandTabsReader.wrap(in, getProject()));
×
1858
                        int matchLine = breaker.findLineIndex(matchOffset);
×
1859
                        if (matchLine >= 0) {
×
1860
                            // Convert to 1-based offset to accord with OpenGrok line number.
1861
                            fragmentIdentifier = String.valueOf(matchLine + 1);
×
1862
                            return true;
×
1863
                        }
1864
                    } catch (IOException e) {
×
1865
                        LOGGER.log(Level.WARNING, String.format("Failed to evaluate match offset for %s",
×
1866
                                resourceFile), e);
1867
                    }
×
1868
                }
1869
            }
1870
        }
1871
        return false;
×
1872
    }
1873
}
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