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

oracle / opengrok / #3672

01 Nov 2023 10:33AM UTC coverage: 66.016% (-9.1%) from 75.13%
#3672

push

web-flow
skip inactive history entries when generating history context (#4461)

fixes #496

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

38690 of 58607 relevant lines covered (66.02%)

0.66 hits per line

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

54.84
/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.Collection;
45
import java.util.Collections;
46
import java.util.Comparator;
47
import java.util.EnumSet;
48
import java.util.List;
49
import java.util.Objects;
50
import java.util.Optional;
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
import java.util.stream.IntStream;
60

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

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

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

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

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

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

131
    // query parameters
132
    static final String PROJECT_PARAM_NAME = "project";
133
    static final String GROUP_PARAM_NAME = "group";
134
    private static final String DEBUG_PARAM_NAME = "debug";
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].isEmpty() || data.rev[1].isEmpty()
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 context = req.getContextPath();
1✔
283
        String[] filepath = new String[2];
1✔
284

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

289
        data.genre = AnalyzerGuru.getGenre(getResourceFile().getName());
1✔
290
        Optional.of(data)
1✔
291
                .filter(this::isTextGenre)
1✔
292
                .ifPresent(textData -> generatePlainTextDiffData(textData, filepath));
1✔
293
       return data;
1✔
294
    }
295

296
    private boolean isTextGenre(DiffData data) {
297
        return data.genre == null || txtGenres.contains(data.genre);
1✔
298
    }
299

300
    private void generatePlainTextDiffData(DiffData data, String[] filepath) {
301
        String srcRoot = getSourceRootPath();
1✔
302
        InputStream[] in = new InputStream[2];
1✔
303
        try {
304
            // Get input stream for both older and newer file.
305
            Future<?>[] future = new Future<?>[2];
1✔
306
            for (int i = 0; i < 2; i++) {
1✔
307
                File f = new File(srcRoot + filepath[i]);
1✔
308
                final String revision = data.rev[i];
1✔
309
                future[i] = executor.submit(() -> HistoryGuru.getInstance().
1✔
310
                        getRevision(f.getParent(), f.getName(), revision));
1✔
311
            }
312

313
            for (int i = 0; i < 2; i++) {
1✔
314
                // The Executor used by given repository will enforce the timeout.
315
                in[i] = (InputStream) future[i].get();
1✔
316
                if (in[i] == null) {
1✔
317
                    data.errorMsg = "Unable to get revision "
1✔
318
                            + Util.htmlize(data.rev[i]) + " for file: "
1✔
319
                            + Util.htmlize(getPath());
1✔
320
                    return;
1✔
321
                }
322
            }
323

324
            /*
325
             * If the genre of the older revision cannot be determined,
326
             * (this can happen if the file was empty), try with newer
327
             * version.
328
             */
329
            populateGenreIfEmpty(data, in);
1✔
330

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

335
            Project p = getProject();
1✔
336
            for (int i = 0; i < 2; i++) {
1✔
337
                // All files under source root are read with UTF-8 as a default.
338
                try (BufferedReader br = new BufferedReader(
1✔
339
                        ExpandTabsReader.wrap(IOUtils.createBOMStrippedReader(
1✔
340
                                in[i], StandardCharsets.UTF_8.name()), p))) {
1✔
341
                    data.file[i] = br.lines().toArray(String[]::new);
1✔
342
                }
343
            }
344
        } catch (Exception e) {
×
345
            data.errorMsg = "Error reading revisions: "
×
346
                    + Util.htmlize(e.getMessage());
×
347
        } finally {
348
            Arrays.stream(in)
1✔
349
                    .forEach(IOUtils::close);
1✔
350
        }
351
        if (Objects.isNull(data.errorMsg)) {
1✔
352
            return;
1✔
353
        }
354
        populateRevisionData(data);
×
355
        populateRevisionURLDetails(data, filepath);
×
356
        data.full = fullDiff();
×
357
        data.type = getDiffType();
×
358
    }
×
359

360
    private void populateGenreIfEmpty(DiffData data, InputStream[] inArray) {
361
        for (int i = 0; i < 2 && data.genre == null; i++) {
1✔
362
            try {
363
                data.genre = AnalyzerGuru.getGenre(inArray[i]);
×
364
            } catch (IOException e) {
×
365
                data.errorMsg = "Unable to determine the file type: "
×
366
                        + Util.htmlize(e.getMessage());
×
367
            }
×
368
        }
369
    }
1✔
370
    private void populateRevisionData(DiffData data) {
371
        try {
372
            data.revision = Diff.diff(data.file[0], data.file[1]);
×
373
        } catch (DifferentiationFailedException e) {
×
374
            data.errorMsg = "Unable to get diffs: " + Util.htmlize(e.getMessage());
×
375
        }
×
376
    }
×
377

378
    private void populateRevisionURLDetails(DiffData data, String[] filePath) {
379
        IntStream.range(0, 2)
×
380
                .forEach(i -> {
×
381
                    try {
382
                        URI u = new URI(null, null, null,
×
383
                                filePath[i] + "@" + data.rev[i], null);
384
                        data.param[i] = u.getRawQuery();
×
385
                    } catch (URISyntaxException e) {
×
386
                        LOGGER.log(Level.WARNING, "Failed to create URI: ", e);
×
387
                    }
×
388
                });
×
389
    }
×
390

391
    /**
392
     * Get the diff display type to use wrt. the request parameter
393
     * {@code format}.
394
     *
395
     * @return {@link DiffType#SIDEBYSIDE} if the request contains no such
396
     * parameter or one with an unknown value, the recognized diff type
397
     * otherwise.
398
     * @see DiffType#get(String)
399
     * @see DiffType#getAbbrev()
400
     * @see DiffType#toString()
401
     */
402
    public DiffType getDiffType() {
403
        DiffType d = DiffType.get(req.getParameter(QueryParameters.FORMAT_PARAM));
×
404
        return d == null ? DiffType.SIDEBYSIDE : d;
×
405
    }
406

407
    /**
408
     * Check, whether a full diff should be displayed.
409
     *
410
     * @return {@code true} if a request parameter {@code full} with the literal
411
     * value {@code 1} was found.
412
     */
413
    public boolean fullDiff() {
414
        String val = req.getParameter(QueryParameters.DIFF_LEVEL_PARAM);
×
415
        return val != null && val.equals("1");
×
416
    }
417

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

454
    /**
455
     * Get a list of filenames in the requested path.
456
     *
457
     * @return an empty list, if the resource does not exist, is not a directory
458
     * or an error occurred when reading it, otherwise a list of filenames in
459
     * that directory, sorted alphabetically
460
     * <p>
461
     * For the root directory (/xref/), authorization check is performed for each
462
     * project in case that projects are used.
463
     * </p>
464
     *
465
     * @see #getResourceFile()
466
     * @see #isDir()
467
     */
468
    public List<String> getResourceFileList() {
469
        if (dirFileList == null) {
1✔
470
            File[] files = null;
1✔
471
            if (isDir() && getResourcePath().length() > 1) {
1✔
472
                files = getResourceFile().listFiles();
1✔
473
            }
474

475
            if (files == null) {
1✔
476
                dirFileList = Collections.emptyList();
×
477
            } else {
478
                List<String> listOfFiles = getSortedFiles(files);
1✔
479

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

500
                dirFileList = Collections.unmodifiableList(listOfFiles);
1✔
501
            }
502
        }
503
        return dirFileList;
1✔
504
    }
505

506
    private static Comparator<File> getFileComparator() {
507
        if (RuntimeEnvironment.getInstance().getListDirsFirst()) {
1✔
508
            return (f1, f2) -> {
1✔
509
                if (f1.isDirectory() && !f2.isDirectory()) {
1✔
510
                    return -1;
1✔
511
                } else if (!f1.isDirectory() && f2.isDirectory()) {
1✔
512
                    return 1;
1✔
513
                } else {
514
                    return f1.getName().compareTo(f2.getName());
1✔
515
                }
516
            };
517
        } else {
518
            return Comparator.comparing(File::getName);
×
519
        }
520
    }
521

522
    @VisibleForTesting
523
    public static List<String> getSortedFiles(File[] files) {
524
        return Arrays.stream(files).sorted(getFileComparator()).map(File::getName).collect(Collectors.toList());
1✔
525
    }
526

527
    /**
528
     * @return the last modification time of the related file or directory.
529
     * @see File#lastModified()
530
     */
531
    public long getLastModified() {
532
        return getResourceFile().lastModified();
×
533
    }
534

535
    /**
536
     * Get all RSS related directories from the request using its {@code also} parameter.
537
     *
538
     * @return an empty string if the requested resource is not a directory, a
539
     * space (<code>' '</code>) separated list of unchecked directory names otherwise.
540
     */
541
    public String getHistoryDirs() {
542
        if (!isDir()) {
×
543
            return "";
×
544
        }
545
        String[] val = req.getParameterValues("also");
×
546
        if (val == null || val.length == 0) {
×
547
            return getPath();
×
548
        }
549
        StringBuilder paths = new StringBuilder(getPath());
×
550
        for (String val1 : val) {
×
551
            paths.append(' ').append(val1);
×
552
        }
553
        return paths.toString();
×
554
    }
555

556
    /**
557
     * Get the integer value of the given request parameter.
558
     *
559
     * @param name name of the parameter to lookup.
560
     * @param defaultValue value to return, if the parameter is not set, is not a number, or is &lt; 0.
561
     * @return the parsed integer value on success, the given default value otherwise.
562
     */
563
    public int getIntParam(String name, int defaultValue) {
564
        int ret = defaultValue;
1✔
565
        String s = req.getParameter(name);
1✔
566
        if (s != null && !s.isEmpty()) {
1✔
567
            try {
568
                int x = Integer.parseInt(s, 10);
1✔
569
                if (x >= 0) {
1✔
570
                    ret = x;
1✔
571
                }
572
            } catch (NumberFormatException e) {
1✔
573
                LOGGER.log(Level.INFO, String.format("Failed to parse %s integer %s", name, s), e);
1✔
574
            }
1✔
575
        }
576
        return ret;
1✔
577
    }
578

579
    /**
580
     * Get the <b>start</b> index for a search result or history listing to return by looking up
581
     * the {@code start} request parameter.
582
     *
583
     * @return 0 if the corresponding start parameter is not set or not a number, the number found otherwise.
584
     */
585
    public int getStartIndex() {
586
        return getIntParam(QueryParameters.START_PARAM, 0);
1✔
587
    }
588

589
    /**
590
     * Get the number of search results or history entries to display by looking up the
591
     * {@code n} request parameter.
592
     *
593
     * @return the default number of items if the corresponding start parameter
594
     * is not set or not a number, the number found otherwise.
595
     */
596
    public int getMaxItems() {
597
        return getIntParam(QueryParameters.COUNT_PARAM, getEnv().getHitsPerPage());
1✔
598
    }
599

600
    public int getRevisionMessageCollapseThreshold() {
601
        return getEnv().getRevisionMessageCollapseThreshold();
×
602
    }
603

604
    public int getCurrentIndexedCollapseThreshold() {
605
        return getEnv().getCurrentIndexedCollapseThreshold();
×
606
    }
607

608
    public int getGroupsCollapseThreshold() {
609
        return getEnv().getGroupsCollapseThreshold();
×
610
    }
611

612
    /**
613
     * Get sort orders from the request parameter {@code sort} and if this list
614
     * would be empty from the cookie {@code OpenGrokSorting}.
615
     *
616
     * @return a possible empty list which contains the sort order values in the
617
     * same order supplied by the request parameter or cookie(s).
618
     */
619
    public List<SortOrder> getSortOrder() {
620
        List<SortOrder> sort = new ArrayList<>();
×
621
        List<String> vals = getParameterValues(QueryParameters.SORT_PARAM);
×
622
        for (String s : vals) {
×
623
            SortOrder so = SortOrder.get(s);
×
624
            if (so != null) {
×
625
                sort.add(so);
×
626
            }
627
        }
×
628
        if (sort.isEmpty()) {
×
629
            vals = getCookieVals("OpenGrokSorting");
×
630
            for (String s : vals) {
×
631
                SortOrder so = SortOrder.get(s);
×
632
                if (so != null) {
×
633
                    sort.add(so);
×
634
                }
635
            }
×
636
        }
637
        return sort;
×
638
    }
639

640
    /**
641
     * Get a reference to the {@code QueryBuilder} wrt. to the current request
642
     * parameters: <dl> <dt>q</dt> <dd>freetext lookup rules</dd> <dt>defs</dt>
643
     * <dd>definitions lookup rules</dd> <dt>path</dt> <dd>path related
644
     * rules</dd> <dt>hist</dt> <dd>history related rules</dd> </dl>
645
     *
646
     * @return a query builder with all relevant fields populated.
647
     */
648
    public QueryBuilder getQueryBuilder() {
649
        if (queryBuilder == null) {
1✔
650
            queryBuilder = new QueryBuilder().
1✔
651
                    setFreetext(Laundromat.launderQuery(req.getParameter(QueryBuilder.FULL)))
1✔
652
                    .setDefs(Laundromat.launderQuery(req.getParameter(QueryBuilder.DEFS)))
1✔
653
                    .setRefs(Laundromat.launderQuery(req.getParameter(QueryBuilder.REFS)))
1✔
654
                    .setPath(Laundromat.launderQuery(req.getParameter(QueryBuilder.PATH)))
1✔
655
                    .setHist(Laundromat.launderQuery(req.getParameter(QueryBuilder.HIST)))
1✔
656
                    .setType(Laundromat.launderQuery(req.getParameter(QueryBuilder.TYPE)));
1✔
657
        }
658

659
        return queryBuilder;
1✔
660
    }
661

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

686
    /**
687
     * Get the definition tag for the request related file or directory.
688
     *
689
     * @return an empty string if not found, the tag otherwise.
690
     */
691
    public String getDefineTagsIndex() {
692
        if (dtag != null) {
×
693
            return dtag;
×
694
        }
695
        getEftarReader();
×
696
        if (eftarReader != null) {
×
697
            try {
698
                dtag = eftarReader.get(getPath());
×
699
            } catch (IOException e) {
×
700
                LOGGER.log(Level.INFO, "Failed to get entry from eftar reader: ", e);
×
701
            }
×
702
        }
703
        if (dtag == null) {
×
704
            dtag = "";
×
705
        }
706
        return dtag;
×
707
    }
708

709
    /**
710
     * Get the revision parameter {@code r} from the request.
711
     *
712
     * @return revision if found, an empty string otherwise.
713
     */
714
    public String getRequestedRevision() {
715
        if (rev == null) {
1✔
716
            String tmp = Laundromat.launderInput(req.getParameter(QueryParameters.REVISION_PARAM));
1✔
717
            rev = (tmp != null && !tmp.isEmpty()) ? tmp : "";
1✔
718
        }
719
        return rev;
1✔
720
    }
721

722
    /**
723
     * Check, whether the request related resource has history information.
724
     *
725
     * @return {@code true} if history is available.
726
     * @see HistoryGuru#hasHistory(File)
727
     */
728
    public boolean hasHistory() {
729
        if (hasHistory == null) {
1✔
730
            hasHistory = HistoryGuru.getInstance().hasHistory(getResourceFile());
1✔
731
        }
732
        return hasHistory;
1✔
733
    }
734

735
    /**
736
     * Check, whether annotations are available for the related resource.
737
     *
738
     * @return {@code true} if annotations are available.
739
     */
740
    public boolean hasAnnotations() {
741
        if (hasAnnotation == null) {
1✔
742
            hasAnnotation = !isDir() && HistoryGuru.getInstance().hasAnnotation(getResourceFile());
1✔
743
        }
744
        return hasAnnotation;
1✔
745
    }
746

747
    /**
748
     * Check, whether the resource to show should be annotated.
749
     *
750
     * @return {@code true} if annotation is desired and available.
751
     */
752
    public boolean annotate() {
753
        if (annotate == null) {
1✔
754
            annotate = hasAnnotations() && Boolean.parseBoolean(req.getParameter(QueryParameters.ANNOTATION_PARAM));
1✔
755
        }
756
        return annotate;
1✔
757
    }
758

759
    /**
760
     * Get the annotation for the requested resource.
761
     *
762
     * @return {@code null} if not available or annotation was not requested,
763
     * the cached annotation otherwise.
764
     */
765
    public Annotation getAnnotation() {
766
        if (isDir() || getResourcePath().equals("/") || !annotate()) {
1✔
767
            return null;
×
768
        }
769
        if (annotation != null) {
1✔
770
            return annotation;
×
771
        }
772
        getRequestedRevision();
1✔
773
        try {
774
            annotation = HistoryGuru.getInstance().annotate(resourceFile, rev.isEmpty() ? null : rev);
1✔
775
        } catch (IOException e) {
×
776
            LOGGER.log(Level.WARNING, "Failed to get annotations: ", e);
×
777
            /* ignore */
778
        }
1✔
779
        return annotation;
1✔
780
    }
781

782
    /**
783
     * Get the {@code path} parameter and display value for "Search only in" option.
784
     *
785
     * @return always an array of 3 fields, whereby field[0] contains the path
786
     * value to use (starts and ends always with a {@link org.opengrok.indexer.index.Indexer#PATH_SEPARATOR}).
787
     * Field[1] the contains string to show in the UI. field[2] is set to {@code disabled=""} if the
788
     * current path is the "/" directory, otherwise set to an empty string.
789
     */
790
    public String[] getSearchOnlyIn() {
791
        if (isDir()) {
×
792
            return getPath().isEmpty()
×
793
                    ? new String[]{"/", "this directory", "disabled=\"\""}
×
794
                    : new String[]{getPath(), "this directory", ""};
×
795
        }
796
        String[] res = new String[3];
×
797
        res[0] = getPath().substring(0, getPath().lastIndexOf(PATH_SEPARATOR) + 1);
×
798
        res[1] = res[0];
×
799
        res[2] = "";
×
800
        return res;
×
801
    }
802

803
    /**
804
     * Get the project {@link #getPath()} refers to.
805
     *
806
     * @return {@code null} if not available, the project otherwise.
807
     */
808
    @Nullable
809
    public Project getProject() {
810
        return Project.getProject(getResourceFile());
1✔
811
    }
812

813
    /**
814
     * Same as {@link #getRequestedProjects()} but returns the project names as
815
     * a coma separated String.
816
     *
817
     * @return a possible empty String but never {@code null}.
818
     */
819
    public String getRequestedProjectsAsString() {
820
        if (requestedProjectsString == null) {
×
821
            requestedProjectsString = String.join(",", getRequestedProjects());
×
822
        }
823
        return requestedProjectsString;
×
824
    }
825

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

866
    private static final Pattern COMMA_PATTERN = Pattern.compile(",");
1✔
867

868
    private static void splitByComma(String value, List<String> result) {
869
        if (value == null || value.isEmpty()) {
1✔
870
            return;
×
871
        }
872
        String[] p = COMMA_PATTERN.split(value);
1✔
873
        for (String p1 : p) {
1✔
874
            if (!p1.isEmpty()) {
1✔
875
                result.add(p1);
1✔
876
            }
877
        }
878
    }
1✔
879

880
    /**
881
     * Get the cookie values for the given name. Splits comma separated values
882
     * automatically into a list of Strings.
883
     *
884
     * @param cookieName name of the cookie.
885
     * @return a possible empty list.
886
     */
887
    public List<String> getCookieVals(String cookieName) {
888
        Cookie[] cookies = req.getCookies();
1✔
889
        ArrayList<String> res = new ArrayList<>();
1✔
890
        if (cookies != null) {
1✔
891
            for (int i = cookies.length - 1; i >= 0; i--) {
1✔
892
                if (cookies[i].getName().equals(cookieName)) {
1✔
893
                    String value = URLDecoder.decode(cookies[i].getValue(), StandardCharsets.UTF_8);
1✔
894
                    splitByComma(value, res);
1✔
895
                }
896
            }
897
        }
898
        return res;
1✔
899
    }
900

901
    /**
902
     * Get the parameter values for the given name. Splits comma separated
903
     * values automatically into a list of Strings.
904
     *
905
     * @param paramName name of the parameter.
906
     * @return a possible empty list.
907
     */
908
    private List<String> getParameterValues(String paramName) {
909
        String[] parameterValues = req.getParameterValues(paramName);
1✔
910
        List<String> res = new ArrayList<>();
1✔
911
        if (parameterValues != null) {
1✔
912
            for (int i = parameterValues.length - 1; i >= 0; i--) {
1✔
913
                splitByComma(parameterValues[i], res);
1✔
914
            }
915
        }
916
        return res;
1✔
917
    }
918

919
    /**
920
     * Same as {@link #getRequestedProjects()}, but with a variable cookieName
921
     * and parameter name.
922
     *
923
     * @param projectParamName the name of the request parameter corresponding to a project name.
924
     * @param groupParamName   the name of the request parameter corresponding to a group name
925
     * @param cookieName       name of the cookie which possible contains project
926
     *                         names used as fallback
927
     * @return set of project names. Possibly empty set but never {@code null}.
928
     */
929
    private SortedSet<String> getRequestedProjects(
930
            String projectParamName,
931
            String groupParamName,
932
            String cookieName
933
    ) {
934

935
        TreeSet<String> projectNames = new TreeSet<>();
1✔
936
        if (Boolean.parseBoolean(req.getParameter(QueryParameters.ALL_PROJECT_SEARCH))) {
1✔
937
            return getProjectHelper()
1✔
938
                    .getAllProjects()
1✔
939
                    .stream()
1✔
940
                    .map(Project::getName)
1✔
941
                    .collect(Collectors.toCollection(TreeSet::new));
1✔
942
        }
943

944
        // Use a project determined directly from the URL
945
        boolean isProjectIndexed = Optional.ofNullable(getProject())
1✔
946
                .map(Project::isIndexed)
1✔
947
                .orElse(false);
1✔
948
        if (isProjectIndexed) {
1✔
949
            projectNames.add(getProject().getName());
×
950
            return projectNames;
×
951
        }
952

953
        List<Project> projects = getEnv().getProjectList();
1✔
954
        if (projects.size() == 1) {
1✔
955
            Project p = projects.get(0);
×
956
            if (isIndexedAndAllowed(p)) {
×
957
                projectNames.add(p.getName());
×
958
            }
959
            return projectNames;
×
960
        }
961

962
        // Add all projects which match the project parameter name values/
963
        getParameterValues(projectParamName)
1✔
964
                .stream()
1✔
965
                .map(Project::getByName)
1✔
966
                .filter(Objects::nonNull)
1✔
967
                .filter(this::isIndexedAndAllowed)
1✔
968
                .map(Project::getName)
1✔
969
                .forEach(projectNames::add);
1✔
970
        var groupNames = getParameterValues(groupParamName)
1✔
971
                .stream()
1✔
972
                .map(Group::getByName)
1✔
973
                .filter(Objects::nonNull)
1✔
974
                .collect(Collectors.toList());
1✔
975
        groupNames.stream()
1✔
976
                .map(group -> getProjectHelper().getAllGrouped(group))
1✔
977
                .flatMap(Collection::stream)
1✔
978
                .filter(Project::isIndexed)
1✔
979
                .map(Project::getName)
1✔
980
                .forEach(projectNames::add);
1✔
981

982
        // Add projects based on cookie.
983
        if (projectNames.isEmpty() && getIntParam(QueryParameters.NUM_SELECTED_PARAM, -1) != 0) {
1✔
984
            getCookieVals(cookieName)
1✔
985
                    .stream()
1✔
986
                    .map(Project::getByName)
1✔
987
                    .filter(Objects::nonNull)
1✔
988
                    .filter(this::isIndexedAndAllowed)
1✔
989
                    .map(Project::getName)
1✔
990
                    .forEach(projectNames::add);
1✔
991
        }
992

993
        // Add default projects.
994
        if (projectNames.isEmpty()) {
1✔
995
            Optional.ofNullable(env.getDefaultProjects())
1✔
996
                    .stream().flatMap(Collection::stream)
1✔
997
                    .filter(this::isIndexedAndAllowed)
1✔
998
                    .map(Project::getName)
1✔
999
                    .forEach(projectNames::add);
1✔
1000
        }
1001

1002
        return projectNames;
1✔
1003
    }
1004

1005
    private boolean isIndexedAndAllowed(Project project) {
1006
        return project.isIndexed() && this.isAllowed(project);
1✔
1007
    }
1008
    public ProjectHelper getProjectHelper() {
1009
        return ProjectHelper.getInstance(this);
1✔
1010
    }
1011

1012
    /**
1013
     * Set the page title to use.
1014
     *
1015
     * @param title title to set (might be {@code null}).
1016
     */
1017
    public void setTitle(String title) {
1018
        pageTitle = title;
×
1019
    }
×
1020

1021
    /**
1022
     * Get the page title to use.
1023
     *
1024
     * @return {@code null} if not set, the page title otherwise.
1025
     */
1026
    public String getTitle() {
1027
        return pageTitle;
×
1028
    }
1029

1030
    /**
1031
     * Get the base path to use to refer to CSS stylesheets and related
1032
     * resources. Usually used to create links.
1033
     *
1034
     * @return the appropriate application directory prefixed with the
1035
     * application's context path (e.g. "/source/default").
1036
     * @see HttpServletRequest#getContextPath()
1037
     * @see RuntimeEnvironment#getWebappLAF()
1038
     */
1039
    public String getCssDir() {
1040
        return req.getContextPath() + PATH_SEPARATOR + getEnv().getWebappLAF();
×
1041
    }
1042

1043
    /**
1044
     * Get the current runtime environment.
1045
     *
1046
     * @return the runtime env.
1047
     * @see RuntimeEnvironment#getInstance()
1048
     */
1049
    public RuntimeEnvironment getEnv() {
1050
        if (env == null) {
1✔
1051
            env = RuntimeEnvironment.getInstance();
1✔
1052
        }
1053
        return env;
1✔
1054
    }
1055

1056
    /**
1057
     * Get the name patterns used to determine, whether a file should be
1058
     * ignored.
1059
     *
1060
     * @return the corresponding value from the current runtime config..
1061
     */
1062
    public IgnoredNames getIgnoredNames() {
1063
        if (ignoredNames == null) {
1✔
1064
            ignoredNames = getEnv().getIgnoredNames();
1✔
1065
        }
1066
        return ignoredNames;
1✔
1067
    }
1068

1069
    /**
1070
     * Get the canonical path to root of the source tree. File separators are
1071
     * replaced with a {@link org.opengrok.indexer.index.Indexer#PATH_SEPARATOR}.
1072
     *
1073
     * @return The on disk source root directory.
1074
     * @see RuntimeEnvironment#getSourceRootPath()
1075
     */
1076
    public String getSourceRootPath() {
1077
        if (sourceRootPath == null) {
1✔
1078
            String srcPathEnv = getEnv().getSourceRootPath();
1✔
1079
            if (srcPathEnv != null) {
1✔
1080
                sourceRootPath = srcPathEnv.replace(File.separatorChar, PATH_SEPARATOR);
1✔
1081
            }
1082
        }
1083
        return sourceRootPath;
1✔
1084
    }
1085

1086
    /**
1087
     * Get the prefix for the related request.
1088
     *
1089
     * @return {@link Prefix#UNKNOWN} if the servlet path matches any known
1090
     * prefix, the prefix otherwise.
1091
     */
1092
    public Prefix getPrefix() {
1093
        if (prefix == null) {
1✔
1094
            prefix = Prefix.get(req.getServletPath());
1✔
1095
        }
1096
        return prefix;
1✔
1097
    }
1098

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

1117
    private void setPath(String path) {
1118
        this.path = Util.getCanonicalPath(Laundromat.launderInput(path), PATH_SEPARATOR);
1✔
1119
    }
1✔
1120

1121
    /**
1122
     * @return true if file/directory corresponding to the request path exists however is unreadable, false otherwise
1123
     */
1124
    public boolean isUnreadable() {
1125
        File file = new File(getSourceRootPath(), getPath());
×
1126
        return file.exists() && !file.canRead();
×
1127
    }
1128

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

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

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

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

1202
    /**
1203
     * Check, whether the request related path represents a directory.
1204
     *
1205
     * @return {@code true} if directory related request
1206
     */
1207
    public boolean isDir() {
1208
        if (isDir == null) {
1✔
1209
            isDir = getResourceFile().isDirectory();
1✔
1210
        }
1211
        return isDir;
1✔
1212
    }
1213

1214
    private static String trailingSlash(String path) {
1215
        return path.length() == 0 || path.charAt(path.length() - 1) != PATH_SEPARATOR
1✔
1216
                ? PATH_SEPARATOR_STRING
1✔
1217
                : "";
1✔
1218
    }
1219

1220
    @Nullable
1221
    private File checkFileInner(File file, File dir, String name) {
1222

1223
        MeterRegistry meterRegistry = Metrics.getRegistry();
×
1224

1225
        File xrefFile = new File(dir, name);
×
1226
        if (xrefFile.exists() && xrefFile.isFile()) {
×
1227
            if (xrefFile.lastModified() >= file.lastModified()) {
×
1228
                if (meterRegistry != null) {
×
1229
                    Counter.builder("xref.file.get").
×
1230
                            description("xref file get hits").
×
1231
                            tag("what", "hits").
×
1232
                            register(meterRegistry).
×
1233
                            increment();
×
1234
                }
1235
                return xrefFile;
×
1236
            } else {
1237
                LOGGER.log(Level.WARNING, "file ''{0}'' is newer than ''{1}''", new Object[]{file, xrefFile});
×
1238
            }
1239
        }
1240

1241
        if (meterRegistry != null) {
×
1242
            Counter.builder("xref.file.get").
×
1243
                    description("xref file get misses").
×
1244
                    tag("what", "miss").
×
1245
                    register(meterRegistry).
×
1246
                    increment();
×
1247
        }
1248

1249
        return null;
×
1250
    }
1251

1252
    @Nullable
1253
    private File checkFile(File file, File dir, String name, boolean compressed) {
1254
        File f;
1255
        if (compressed) {
×
1256
            f = checkFileInner(file, dir, TandemPath.join(name, ".gz"));
×
1257
            if (f != null) {
×
1258
                return f;
×
1259
            }
1260
        }
1261

1262
        return checkFileInner(file, dir, name);
×
1263
    }
1264

1265
    @Nullable
1266
    private File checkFileResolve(File dir, String name, boolean compressed) {
1267
        File lresourceFile = new File(getSourceRootPath() + getPath(), name);
×
1268
        if (!lresourceFile.canRead()) {
×
1269
            lresourceFile = new File(PATH_SEPARATOR_STRING);
×
1270
        }
1271

1272
        return checkFile(lresourceFile, dir, name, compressed);
×
1273
    }
1274

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

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

1318
    /**
1319
     * Is revision the latest revision ?
1320
     * @param rev revision string
1321
     * @return true if latest revision, false otherwise
1322
     */
1323
    public boolean isLatestRevision(String rev) {
1324
        return rev.equals(getLatestRevision(getResourceFile()));
×
1325
    }
1326

1327
    /**
1328
     * Get the location of cross-reference for given file containing the given revision.
1329
     * @param revStr defined revision string
1330
     * @return location to redirect to
1331
     */
1332
    public String getRevisionLocation(String revStr) {
1333
        StringBuilder sb = new StringBuilder();
1✔
1334

1335
        sb.append(req.getContextPath());
1✔
1336
        sb.append(Prefix.XREF_P);
1✔
1337
        sb.append(Util.uriEncodePath(getPath()));
1✔
1338
        sb.append("?");
1✔
1339
        sb.append(QueryParameters.REVISION_PARAM_EQ);
1✔
1340
        sb.append(Util.uriEncode(revStr));
1✔
1341

1342
        if (req.getQueryString() != null) {
1✔
1343
            sb.append("&");
1✔
1344
            sb.append(req.getQueryString());
1✔
1345
        }
1346
        if (fragmentIdentifier != null) {
1✔
1347
            String anchor = Util.uriEncode(fragmentIdentifier);
×
1348

1349
            String reqFrag = req.getParameter(QueryParameters.FRAGMENT_IDENTIFIER_PARAM);
×
1350
            if (reqFrag == null || reqFrag.isEmpty()) {
×
1351
                /*
1352
                 * We've determined that the fragmentIdentifier field must have
1353
                 * been set to augment request parameters. Now include it
1354
                 * explicitly in the next request parameters.
1355
                 */
1356
                sb.append("&");
×
1357
                sb.append(QueryParameters.FRAGMENT_IDENTIFIER_PARAM_EQ);
×
1358
                sb.append(anchor);
×
1359
            }
1360
            sb.append("#");
×
1361
            sb.append(anchor);
×
1362
        }
1363

1364
        return sb.toString();
1✔
1365
    }
1366

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

1383
            if (getPath().length() == 0) {
1✔
1384
                // => /
1385
                return null;
×
1386
            }
1387

1388
            if (prefix != Prefix.XREF_P && prefix != Prefix.HIST_L
1✔
1389
                    && prefix != Prefix.RSS_P) {
1390
                // if it is an existing dir perhaps people wanted dir xref
1391
                return req.getContextPath() + Prefix.XREF_P
×
1392
                        + getUriEncodedPath() + trailingSlash(getPath());
×
1393
            }
1394
            String ts = trailingSlash(getPath());
1✔
1395
            if (ts.length() != 0) {
1✔
1396
                return req.getContextPath() + prefix + getUriEncodedPath() + ts;
1✔
1397
            }
1398
        }
1399
        return null;
1✔
1400
    }
1401

1402
    /**
1403
     * Get the URI encoded canonical path to the related file or directory (the
1404
     * URI part between the servlet path and the start of the query string).
1405
     *
1406
     * @return a URI encoded path which might be an empty string but not {@code null}.
1407
     * @see #getPath()
1408
     */
1409
    public String getUriEncodedPath() {
1410
        if (uriEncodedPath == null) {
1✔
1411
            uriEncodedPath = Util.uriEncodePath(getPath());
1✔
1412
        }
1413
        return uriEncodedPath;
1✔
1414
    }
1415

1416
    /**
1417
     * Add a new file script to the page by the name.
1418
     *
1419
     * @param name name of the script to search for
1420
     * @return this
1421
     *
1422
     * @see Scripts#addScript(String, String, Scripts.Type)
1423
     */
1424
    public PageConfig addScript(String name) {
1425
        this.scripts.addScript(this.req.getContextPath(), name, isDebug() ? Scripts.Type.DEBUG : Scripts.Type.MINIFIED);
×
1426
        return this;
×
1427
    }
1428

1429
    private boolean isDebug() {
1430
        return Boolean.parseBoolean(req.getParameter(DEBUG_PARAM_NAME));
×
1431
    }
1432

1433
    /**
1434
     * Return the page scripts.
1435
     *
1436
     * @return the scripts
1437
     *
1438
     * @see Scripts
1439
     */
1440
    public Scripts getScripts() {
1441
        return this.scripts;
×
1442
    }
1443

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

1469
    /**
1470
     * Play nice in reverse proxy environment by using pre-configured hostname request to construct the URLs.
1471
     * Will not work well if the scheme or port is different for proxied server and original server.
1472
     * @return server name
1473
     */
1474
    public String getServerName() {
1475
        if (env.getServerName() != null) {
×
1476
            return env.getServerName();
×
1477
        } else {
1478
            return req.getServerName();
×
1479
        }
1480
    }
1481

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

1498
        if (getRequestedProjects().isEmpty() && getEnv().hasProjects()) {
×
1499
            sh.setErrorMsg("You must select a project!");
×
1500
            return sh;
×
1501
        }
1502

1503
        if (sh.getBuilder().getSize() == 0) {
×
1504
            // Entry page show the map
1505
            sh.setRedirect(req.getContextPath() + '/');
×
1506
            return sh;
×
1507
        }
1508

1509
        return sh;
×
1510
    }
1511

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

1531
    /**
1532
     * @param project {@link Project} instance or {@code null}
1533
     * @param request HTTP request
1534
     * @return list of {@link NullableNumLinesLOC} instances related to {@link #path}
1535
     * @throws IOException on I/O error
1536
     */
1537
    @Nullable
1538
    public List<NullableNumLinesLOC> getExtras(@Nullable Project project, HttpServletRequest request)
1539
            throws IOException {
1540

1541
        SearchHelper searchHelper = prepareInternalSearch(SortOrder.RELEVANCY);
1✔
1542
        /*
1543
         * N.b. searchHelper.destroy() is called via
1544
         * WebappListener.requestDestroyed() on presence of the following
1545
         * REQUEST_ATTR.
1546
         */
1547
        request.setAttribute(SearchHelper.REQUEST_ATTR, searchHelper);
1✔
1548
        Optional.ofNullable(project)
1✔
1549
                .ifPresentOrElse(searchHelper::prepareExec,
1✔
1550
                        () -> searchHelper.prepareExec(new TreeSet<>())
×
1551
                );
1552
        return getNullableNumLinesLOCS(project, searchHelper);
1✔
1553
    }
1554

1555
    @Nullable
1556
    @VisibleForTesting
1557
    public List<NullableNumLinesLOC> getNullableNumLinesLOCS(@Nullable Project project, SearchHelper searchHelper)
1558
            throws IOException {
1559

1560
        if (searchHelper.getSearcher() != null) {
1✔
1561
            DirectoryExtraReader extraReader = new DirectoryExtraReader();
1✔
1562
            String primePath = path;
1✔
1563
            try {
1564
                primePath = searchHelper.getPrimeRelativePath(project != null ? project.getName() : "", path);
1✔
1565
            } catch (IOException | ForbiddenSymlinkException ex) {
×
1566
                LOGGER.log(Level.WARNING, String.format(
×
1567
                        "Error getting prime relative for %s", path), ex);
1568
            }
1✔
1569
            return extraReader.search(searchHelper.getSearcher(), primePath);
1✔
1570
        }
1571

1572
        return null;
×
1573
    }
1574

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

1594
    public static PageConfig get(String path, HttpServletRequest request) {
1595
        PageConfig pcfg = new PageConfig(request);
1✔
1596
        pcfg.setPath(path);
1✔
1597
        return pcfg;
1✔
1598
    }
1599

1600
    private PageConfig() {
1✔
1601
        this.authFramework = RuntimeEnvironment.getInstance().getAuthorizationFramework();
1✔
1602
        this.executor = RuntimeEnvironment.getInstance().getRevisionExecutor();
1✔
1603
    }
1✔
1604

1605
    private PageConfig(HttpServletRequest req) {
1606
        this();
1✔
1607

1608
        this.req = req;
1✔
1609
        this.fragmentIdentifier = req.getParameter(QueryParameters.FRAGMENT_IDENTIFIER_PARAM);
1✔
1610
    }
1✔
1611

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

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

1645
    /**
1646
     * Checks if current request is allowed to access group.
1647
     * @param g group
1648
     * @return true if yes
1649
     */
1650
    public boolean isAllowed(Group g) {
1651
        return this.authFramework.isAllowed(this.req, g);
1✔
1652
    }
1653

1654

1655
    public SortedSet<AcceptedMessage> getMessages() {
1656
        return env.getMessages();
×
1657
    }
1658

1659
    public SortedSet<AcceptedMessage> getMessages(String tag) {
1660
        return env.getMessages(tag);
×
1661
    }
1662

1663
    /**
1664
     * Get basename of the path or "/" if the path is empty.
1665
     * This is used for setting title of various pages.
1666
     * @param path path
1667
     * @return short version of the path
1668
     */
1669
    public String getShortPath(String path) {
1670
        File file = new File(path);
×
1671

1672
        if (path.isEmpty()) {
×
1673
            return "/";
×
1674
        }
1675

1676
        return file.getName();
×
1677
    }
1678

1679
    private String addTitleDelimiter(String title) {
1680
        if (!title.isEmpty()) {
×
1681
            return title + ", ";
×
1682
        }
1683

1684
        return title;
×
1685
    }
1686

1687
    /**
1688
     * The search page title string should progressively reflect the search terms
1689
     * so that if only small portion of the string is seen, it describes
1690
     * the action as closely as possible while remaining readable.
1691
     * @return string used for setting page title of search results page
1692
     */
1693
    public String getSearchTitle() {
1694
        String title = "";
×
1695

1696
        if (req.getParameter(QueryBuilder.FULL) != null && !req.getParameter(QueryBuilder.FULL).isEmpty()) {
×
1697
            title += req.getParameter(QueryBuilder.FULL) + " (full)";
×
1698
        }
1699
        if (req.getParameter(QueryBuilder.DEFS) != null && !req.getParameter(QueryBuilder.DEFS).isEmpty()) {
×
1700
            title = addTitleDelimiter(title);
×
1701
            title += req.getParameter(QueryBuilder.DEFS) + " (definition)";
×
1702
        }
1703
        if (req.getParameter(QueryBuilder.REFS) != null && !req.getParameter(QueryBuilder.REFS).isEmpty()) {
×
1704
            title = addTitleDelimiter(title);
×
1705
            title += req.getParameter(QueryBuilder.REFS) + " (reference)";
×
1706
        }
1707
        if (req.getParameter(QueryBuilder.PATH) != null && !req.getParameter(QueryBuilder.PATH).isEmpty()) {
×
1708
            title = addTitleDelimiter(title);
×
1709
            title += req.getParameter(QueryBuilder.PATH) + " (path)";
×
1710
        }
1711
        if (req.getParameter(QueryBuilder.HIST) != null && !req.getParameter(QueryBuilder.HIST).isEmpty()) {
×
1712
            title = addTitleDelimiter(title);
×
1713
            title += req.getParameter(QueryBuilder.HIST) + " (history)";
×
1714
        }
1715

1716
        if (req.getParameterValues(QueryBuilder.PROJECT) != null && req.getParameterValues(QueryBuilder.PROJECT).length != 0) {
×
1717
            if (!title.isEmpty()) {
×
1718
                title += " ";
×
1719
            }
1720
            title += "in projects: ";
×
1721
            String[] projects = req.getParameterValues(QueryBuilder.PROJECT);
×
1722
            title += String.join(",", projects);
×
1723
        }
1724

1725
        return Util.htmlize(title + " - OpenGrok search results");
×
1726
    }
1727

1728
    /**
1729
     * Similar as {@link #getSearchTitle()}.
1730
     * @return string used for setting page title of search view
1731
     */
1732
    public String getHistoryTitle() {
1733
        String strPath = getPath();
×
1734
        return Util.htmlize(getShortPath(strPath) + " - OpenGrok history log for " + strPath);
×
1735
    }
1736

1737
    public String getPathTitle() {
1738
        String strPath = getPath();
×
1739
        String title = getShortPath(strPath);
×
1740
        if (!getRequestedRevision().isEmpty()) {
×
1741
            title += " (revision " + getRequestedRevision() + ")";
×
1742
        }
1743
        title += " - OpenGrok cross reference for " + (strPath.isEmpty() ? "/" : strPath);
×
1744

1745
        return Util.htmlize(title);
×
1746
    }
1747

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

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

1781
        if (getProject() != null) {
×
1782
            messages.addAll(getMessages(getProject().getName()));
×
1783
            getProject().getGroups()
×
1784
                    .stream()
×
1785
                    .map(Group::getName)
×
1786
                    .map(this::getMessages)
×
1787
                    .forEach(messages::addAll);
×
1788
        }
1789

1790
        return messages;
×
1791
    }
1792

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

1830
        String headerEtag = request.getHeader(HttpHeaders.IF_NONE_MATCH);
×
1831

1832
        if (headerEtag != null && headerEtag.equals(currentEtag)) {
×
1833
            // weak ETag has not changed, return 304 NOT MODIFIED
1834
            response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
×
1835
            return true;
×
1836
        }
1837

1838
        // return 200 OK
1839
        response.setHeader(HttpHeaders.ETAG, currentEtag);
×
1840
        return false;
×
1841
    }
1842

1843
    /**
1844
     * @param root root path
1845
     * @param path path
1846
     * @return path relative to root
1847
     */
1848
    public static String getRelativePath(String root, String path) {
1849
        return Paths.get(root).relativize(Paths.get(path)).toString();
×
1850
    }
1851

1852
    /**
1853
     * Determines whether a match offset from a search result has been indicated,
1854
     * and if so tries to calculate a translated xref fragment identifier.
1855
     * @return {@code true} if a xref fragment identifier was calculated by the call to this method
1856
     */
1857
    public boolean evaluateMatchOffset() {
1858
        if (fragmentIdentifier == null) {
×
1859
            int matchOffset = getIntParam(QueryParameters.MATCH_OFFSET_PARAM, -1);
×
1860
            if (matchOffset >= 0) {
×
1861
                File objResourceFile = getResourceFile();
×
1862
                if (objResourceFile.isFile()) {
×
1863
                    LineBreaker breaker = new LineBreaker();
×
1864
                    StreamSource streamSource = StreamSource.fromFile(objResourceFile);
×
1865
                    try {
1866
                        breaker.reset(streamSource, in -> ExpandTabsReader.wrap(in, getProject()));
×
1867
                        int matchLine = breaker.findLineIndex(matchOffset);
×
1868
                        if (matchLine >= 0) {
×
1869
                            // Convert to 1-based offset to accord with OpenGrok line number.
1870
                            fragmentIdentifier = String.valueOf(matchLine + 1);
×
1871
                            return true;
×
1872
                        }
1873
                    } catch (IOException e) {
×
1874
                        LOGGER.log(Level.WARNING, String.format("Failed to evaluate match offset for %s",
×
1875
                                objResourceFile), e);
1876
                    }
×
1877
                }
1878
            }
1879
        }
1880
        return false;
×
1881
    }
1882
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc