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

oracle / opengrok / #3691

09 Nov 2023 04:15PM UTC coverage: 74.721% (+8.6%) from 66.08%
#3691

push

web-flow
avoid annotations for binary files (#4476)

fixes #4473

6 of 13 new or added lines in 4 files covered. (46.15%)

258 existing lines in 28 files now uncovered.

43797 of 58614 relevant lines covered (74.72%)

0.75 hits per line

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

57.97
/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
            populateRevisionData(data);
1✔
353
            populateRevisionURLDetails(data, filepath);
1✔
354
            data.full = fullDiff();
1✔
355
            data.type = getDiffType();
1✔
356
        }
357
    }
1✔
358

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

602
    public int getRevisionMessageCollapseThreshold() {
603
        return getEnv().getRevisionMessageCollapseThreshold();
×
604
    }
605

606
    public int getCurrentIndexedCollapseThreshold() {
607
        return getEnv().getCurrentIndexedCollapseThreshold();
×
608
    }
609

610
    public int getGroupsCollapseThreshold() {
611
        return getEnv().getGroupsCollapseThreshold();
×
612
    }
613

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

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

661
        return queryBuilder;
1✔
662
    }
663

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

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

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

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

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

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

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

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

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

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

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

868
    private static final Pattern COMMA_PATTERN = Pattern.compile(",");
1✔
869

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

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

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

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

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

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

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

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

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

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

1004
        return projectNames;
1✔
1005
    }
1006

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1222
    @Nullable
1223
    private File checkFileInner(File file, File dir, String name) {
1224

1225
        MeterRegistry meterRegistry = Metrics.getRegistry();
×
1226

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

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

1251
        return null;
×
1252
    }
1253

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

1264
        return checkFileInner(file, dir, name);
×
1265
    }
1266

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

1274
        return checkFile(lresourceFile, dir, name, compressed);
×
1275
    }
1276

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

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

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

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

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

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

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

1366
        return sb.toString();
1✔
1367
    }
1368

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

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

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

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

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

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

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

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

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

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

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

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

1511
        return sh;
×
1512
    }
1513

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

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

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

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

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

1574
        return null;
×
1575
    }
1576

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

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

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

1607
    private PageConfig(HttpServletRequest req) {
1608
        this();
1✔
1609

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

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

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

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

1656

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

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

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

1674
        if (path.isEmpty()) {
×
1675
            return "/";
×
1676
        }
1677

1678
        return file.getName();
×
1679
    }
1680

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

1686
        return title;
×
1687
    }
1688

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

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

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

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

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

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

1747
        return Util.htmlize(title);
×
1748
    }
1749

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

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

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

1792
        return messages;
×
1793
    }
1794

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

1832
        String headerEtag = request.getHeader(HttpHeaders.IF_NONE_MATCH);
×
1833

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

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

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

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