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

oracle / opengrok / #3662

26 Oct 2023 08:39AM UTC coverage: 66.013% (-9.1%) from 75.137%
#3662

push

vladak
propagate the error rather than log it

fixes #4411

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

38693 of 58614 relevant lines covered (66.01%)

0.66 hits per line

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

55.52
/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.Optional;
50
import java.util.Set;
51
import java.util.SortedSet;
52
import java.util.TreeSet;
53
import java.util.concurrent.ExecutorService;
54
import java.util.concurrent.Future;
55
import java.util.logging.Level;
56
import java.util.logging.Logger;
57
import java.util.regex.Pattern;
58
import java.util.stream.Collectors;
59

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

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

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

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

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

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

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

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

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

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

172
    private final ExecutorService executor;
173

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

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

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

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

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

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

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

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

267
        return true;
1✔
268
    }
269

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

589
    public int getRevisionMessageCollapseThreshold() {
590
        return getEnv().getRevisionMessageCollapseThreshold();
×
591
    }
592

593
    public int getCurrentIndexedCollapseThreshold() {
594
        return getEnv().getCurrentIndexedCollapseThreshold();
×
595
    }
596

597
    public int getGroupsCollapseThreshold() {
598
        return getEnv().getGroupsCollapseThreshold();
×
599
    }
600

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

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

648
        return queryBuilder;
1✔
649
    }
650

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

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

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

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

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

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

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

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

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

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

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

855
    private static final Pattern COMMA_PATTERN = Pattern.compile(",");
1✔
856

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

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

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

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

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

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

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

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

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

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

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

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

997
        return projectNames;
1✔
998
    }
999

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1212
    @Nullable
1213
    private File checkFileInner(File file, File dir, String name) {
1214

1215
        MeterRegistry meterRegistry = Metrics.getRegistry();
×
1216

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

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

1241
        return null;
×
1242
    }
1243

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

1254
        return checkFileInner(file, dir, name);
×
1255
    }
1256

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

1264
        return checkFile(lresourceFile, dir, name, compressed);
×
1265
    }
1266

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

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

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

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

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

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

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

1356
        return sb.toString();
1✔
1357
    }
1358

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

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

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

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

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

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

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

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

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

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

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

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

1501
        return sh;
×
1502
    }
1503

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

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

1533
        SearchHelper searchHelper = prepareInternalSearch(SortOrder.RELEVANCY);
1✔
1534
        /*
1535
         * N.b. searchHelper.destroy() is called via
1536
         * WebappListener.requestDestroyed() on presence of the following
1537
         * REQUEST_ATTR.
1538
         */
1539
        request.setAttribute(SearchHelper.REQUEST_ATTR, searchHelper);
1✔
1540
        Optional.ofNullable(project)
1✔
1541
                .ifPresentOrElse(searchHelper::prepareExec,
1✔
1542
                        () -> searchHelper.prepareExec(new TreeSet<>())
×
1543
                );
1544
        return getNullableNumLinesLOCS(project, searchHelper);
1✔
1545
    }
1546

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

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

1564
        return null;
×
1565
    }
1566

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

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

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

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

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

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

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

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

1646

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

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

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

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

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

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

1676
        return title;
×
1677
    }
1678

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

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

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

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

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

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

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

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

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

1773
        if (getProject() != null) {
×
1774
            messages.addAll(getMessages(getProject().getName()));
×
1775
            getProject().getGroups()
×
1776
                    .stream()
×
1777
                    .map(Group::getName)
×
1778
                    .map(this::getMessages)
×
1779
                    .forEach(messages::addAll);
×
1780
        }
1781

1782
        return messages;
×
1783
    }
1784

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

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

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

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

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

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