xref: /OpenGrok/opengrok-web/src/main/java/org/opengrok/web/PageConfig.java (revision c4b39070085d99df7ea64962c445c3ac0db8b13e)
1d8a7afe2SAdam Hornacek /*
2d8a7afe2SAdam Hornacek  * CDDL HEADER START
3d8a7afe2SAdam Hornacek  *
4d8a7afe2SAdam Hornacek  * The contents of this file are subject to the terms of the
5d8a7afe2SAdam Hornacek  * Common Development and Distribution License (the "License").
6d8a7afe2SAdam Hornacek  * You may not use this file except in compliance with the License.
7d8a7afe2SAdam Hornacek  *
8d8a7afe2SAdam Hornacek  * See LICENSE.txt included in this distribution for the specific
9d8a7afe2SAdam Hornacek  * language governing permissions and limitations under the License.
10d8a7afe2SAdam Hornacek  *
11d8a7afe2SAdam Hornacek  * When distributing Covered Code, include this CDDL HEADER in each
12d8a7afe2SAdam Hornacek  * file and include the License file at LICENSE.txt.
13d8a7afe2SAdam Hornacek  * If applicable, add the following below this CDDL HEADER, with the
14d8a7afe2SAdam Hornacek  * fields enclosed by brackets "[]" replaced with your own identifying
15d8a7afe2SAdam Hornacek  * information: Portions Copyright [yyyy] [name of copyright owner]
16d8a7afe2SAdam Hornacek  *
17d8a7afe2SAdam Hornacek  * CDDL HEADER END
18d8a7afe2SAdam Hornacek  */
19d8a7afe2SAdam Hornacek 
20d8a7afe2SAdam Hornacek /*
21a964b78cSVladimir Kotal  * Copyright (c) 2011, 2022, Oracle and/or its affiliates. All rights reserved.
225d9f3aa0SAdam Hornáček  * Portions Copyright (c) 2011, Jens Elkner.
235d9f3aa0SAdam Hornáček  * Portions Copyright (c) 2017, 2020, Chris Fraire <cfraire@me.com>.
24d8a7afe2SAdam Hornacek  */
25d8a7afe2SAdam Hornacek package org.opengrok.web;
26d8a7afe2SAdam Hornacek 
27d8a7afe2SAdam Hornacek import static org.opengrok.indexer.index.Indexer.PATH_SEPARATOR;
28d8a7afe2SAdam Hornacek import static org.opengrok.indexer.index.Indexer.PATH_SEPARATOR_STRING;
29d8a7afe2SAdam Hornacek 
30d8a7afe2SAdam Hornacek import java.io.BufferedReader;
31d8a7afe2SAdam Hornacek import java.io.File;
32d8a7afe2SAdam Hornacek import java.io.FileNotFoundException;
33d8a7afe2SAdam Hornacek import java.io.IOException;
34d8a7afe2SAdam Hornacek import java.io.InputStream;
35d8a7afe2SAdam Hornacek import java.net.URI;
36d8a7afe2SAdam Hornacek import java.net.URISyntaxException;
37d8a7afe2SAdam Hornacek import java.net.URLDecoder;
38d8a7afe2SAdam Hornacek import java.nio.charset.StandardCharsets;
39d8a7afe2SAdam Hornacek import java.nio.file.Paths;
40d8a7afe2SAdam Hornacek import java.security.InvalidParameterException;
41df712980SVladimir Kotal import java.text.ParseException;
42d8a7afe2SAdam Hornacek import java.util.ArrayList;
43d8a7afe2SAdam Hornacek import java.util.Arrays;
44d8a7afe2SAdam Hornacek import java.util.Collections;
45d8a7afe2SAdam Hornacek import java.util.Comparator;
468ff8eecbSVladimir Kotal import java.util.Date;
47d8a7afe2SAdam Hornacek import java.util.EnumSet;
48d8a7afe2SAdam Hornacek import java.util.List;
49d8a7afe2SAdam Hornacek import java.util.Objects;
50d8a7afe2SAdam Hornacek import java.util.Set;
51d8a7afe2SAdam Hornacek import java.util.SortedSet;
52d8a7afe2SAdam Hornacek import java.util.TreeSet;
53d8a7afe2SAdam Hornacek import java.util.concurrent.ExecutorService;
54d8a7afe2SAdam Hornacek import java.util.concurrent.Future;
55d8a7afe2SAdam Hornacek import java.util.logging.Level;
56d8a7afe2SAdam Hornacek import java.util.logging.Logger;
57d8a7afe2SAdam Hornacek import java.util.regex.Pattern;
58d8a7afe2SAdam Hornacek import java.util.stream.Collectors;
59d8a7afe2SAdam Hornacek 
60aa6abf42SAdam Hornacek import jakarta.servlet.ServletRequest;
61aa6abf42SAdam Hornacek import jakarta.servlet.http.Cookie;
62aa6abf42SAdam Hornacek import jakarta.servlet.http.HttpServletRequest;
63aa6abf42SAdam Hornacek import jakarta.servlet.http.HttpServletResponse;
64aa6abf42SAdam Hornacek import jakarta.ws.rs.core.HttpHeaders;
658ff8eecbSVladimir Kotal import org.apache.lucene.document.DateTools;
6636b6a6f8SVladimir Kotal import org.apache.lucene.document.Document;
6736b6a6f8SVladimir Kotal import org.jetbrains.annotations.Nullable;
6834c930beSVladimir Kotal import org.jetbrains.annotations.VisibleForTesting;
69d8a7afe2SAdam Hornacek import org.opengrok.indexer.Info;
70d8a7afe2SAdam Hornacek import org.opengrok.indexer.analysis.AbstractAnalyzer;
71d8a7afe2SAdam Hornacek import org.opengrok.indexer.analysis.AnalyzerGuru;
72d8a7afe2SAdam Hornacek import org.opengrok.indexer.analysis.ExpandTabsReader;
73d8a7afe2SAdam Hornacek import org.opengrok.indexer.analysis.StreamSource;
74d8a7afe2SAdam Hornacek import org.opengrok.indexer.authorization.AuthorizationFramework;
75d8a7afe2SAdam Hornacek import org.opengrok.indexer.configuration.Group;
76d8a7afe2SAdam Hornacek import org.opengrok.indexer.configuration.IgnoredNames;
77d8a7afe2SAdam Hornacek import org.opengrok.indexer.configuration.Project;
78d8a7afe2SAdam Hornacek import org.opengrok.indexer.configuration.RuntimeEnvironment;
79d8a7afe2SAdam Hornacek import org.opengrok.indexer.history.Annotation;
80d8a7afe2SAdam Hornacek import org.opengrok.indexer.history.HistoryEntry;
81d8a7afe2SAdam Hornacek import org.opengrok.indexer.history.HistoryException;
82d8a7afe2SAdam Hornacek import org.opengrok.indexer.history.HistoryGuru;
8336b6a6f8SVladimir Kotal import org.opengrok.indexer.index.IndexDatabase;
84d8a7afe2SAdam Hornacek import org.opengrok.indexer.logger.LoggerFactory;
85d8a7afe2SAdam Hornacek import org.opengrok.indexer.search.QueryBuilder;
86d8a7afe2SAdam Hornacek import org.opengrok.indexer.util.IOUtils;
87d8a7afe2SAdam Hornacek import org.opengrok.indexer.util.LineBreaker;
88d8a7afe2SAdam Hornacek import org.opengrok.indexer.util.TandemPath;
89d8a7afe2SAdam Hornacek import org.opengrok.indexer.web.EftarFileReader;
90d8a7afe2SAdam Hornacek import org.opengrok.indexer.web.Laundromat;
91d8a7afe2SAdam Hornacek import org.opengrok.indexer.web.Prefix;
92d8a7afe2SAdam Hornacek import org.opengrok.indexer.web.QueryParameters;
93d8a7afe2SAdam Hornacek import org.opengrok.indexer.web.SearchHelper;
94d8a7afe2SAdam Hornacek import org.opengrok.indexer.web.SortOrder;
95d8a7afe2SAdam Hornacek import org.opengrok.indexer.web.Util;
96d8a7afe2SAdam Hornacek import org.opengrok.indexer.web.messages.MessagesContainer.AcceptedMessage;
97d8a7afe2SAdam Hornacek import org.suigeneris.jrcs.diff.Diff;
98d8a7afe2SAdam Hornacek import org.suigeneris.jrcs.diff.DifferentiationFailedException;
99d8a7afe2SAdam Hornacek 
100d8a7afe2SAdam Hornacek /**
101d8a7afe2SAdam Hornacek  * A simple container to lazy initialize common vars wrt. a single request. It
102d8a7afe2SAdam Hornacek  * MUST NOT be shared between several requests and
103d8a7afe2SAdam Hornacek  * {@link #cleanup(ServletRequest)} should be called before the page context
104d8a7afe2SAdam Hornacek  * gets destroyed (e.g.when leaving the {@code service} method).
105d8a7afe2SAdam Hornacek  * <p>
106d8a7afe2SAdam Hornacek  * Purpose is to decouple implementation details from web design, so that the
107d8a7afe2SAdam Hornacek  * JSP developer does not need to know every implementation detail and normally
108d8a7afe2SAdam Hornacek  * has to deal with this class/wrapper, only (so some people may like to call
109379f387bSVladimir Kotal  * this class a bean with request scope ;-)). Furthermore, it helps to keep the
110d8a7afe2SAdam Hornacek  * pages (how content gets generated) consistent and to document the request
111d8a7afe2SAdam Hornacek  * parameters used.
112d8a7afe2SAdam Hornacek  * <p>
113d8a7afe2SAdam Hornacek  * General contract for this class (i.e. if not explicitly documented): no
114d8a7afe2SAdam Hornacek  * method of this class changes neither the request nor the response.
115d8a7afe2SAdam Hornacek  *
116d8a7afe2SAdam Hornacek  * @author Jens Elkner
117d8a7afe2SAdam Hornacek  */
118d8a7afe2SAdam Hornacek public final class PageConfig {
119d8a7afe2SAdam Hornacek 
120d8a7afe2SAdam Hornacek     private static final Logger LOGGER = LoggerFactory.getLogger(PageConfig.class);
121d8a7afe2SAdam Hornacek 
122d8a7afe2SAdam Hornacek     // cookie name
123d8a7afe2SAdam Hornacek     public static final String OPEN_GROK_PROJECT = "OpenGrokProject";
124d8a7afe2SAdam Hornacek 
1257c7653c2SVladimir Kotal     public static final String DUMMY_REVISION = "unknown";
1267c7653c2SVladimir Kotal 
127d8a7afe2SAdam Hornacek     // query parameters
128379f387bSVladimir Kotal     static final String PROJECT_PARAM_NAME = "project";
129379f387bSVladimir Kotal     static final String GROUP_PARAM_NAME = "group";
130d8a7afe2SAdam Hornacek     private static final String DEBUG_PARAM_NAME = "debug";
131d8a7afe2SAdam Hornacek 
132d8a7afe2SAdam Hornacek     // TODO if still used, get it from the app context
133d8a7afe2SAdam Hornacek 
134d8a7afe2SAdam Hornacek     private final AuthorizationFramework authFramework;
135d8a7afe2SAdam Hornacek     private RuntimeEnvironment env;
136d8a7afe2SAdam Hornacek     private IgnoredNames ignoredNames;
137d8a7afe2SAdam Hornacek     private String path;
138d8a7afe2SAdam Hornacek     private File resourceFile;
139d8a7afe2SAdam Hornacek     private String resourcePath;
140d8a7afe2SAdam Hornacek     private EftarFileReader eftarReader;
141d8a7afe2SAdam Hornacek     private String sourceRootPath;
142d8a7afe2SAdam Hornacek     private Boolean isDir;
143d8a7afe2SAdam Hornacek     private String uriEncodedPath;
144d8a7afe2SAdam Hornacek     private Prefix prefix;
145d8a7afe2SAdam Hornacek     private String pageTitle;
146d8a7afe2SAdam Hornacek     private String dtag;
147d8a7afe2SAdam Hornacek     private String rev;
148d8a7afe2SAdam Hornacek     private String fragmentIdentifier; // Also settable via match offset translation
149d8a7afe2SAdam Hornacek     private Boolean hasAnnotation;
150d8a7afe2SAdam Hornacek     private Boolean annotate;
151d8a7afe2SAdam Hornacek     private Annotation annotation;
152d8a7afe2SAdam Hornacek     private Boolean hasHistory;
153d8a7afe2SAdam Hornacek     private static final EnumSet<AbstractAnalyzer.Genre> txtGenres
154d8a7afe2SAdam Hornacek             = EnumSet.of(AbstractAnalyzer.Genre.DATA, AbstractAnalyzer.Genre.PLAIN, AbstractAnalyzer.Genre.HTML);
155d8a7afe2SAdam Hornacek     private SortedSet<String> requestedProjects;
156d8a7afe2SAdam Hornacek     private String requestedProjectsString;
157d8a7afe2SAdam Hornacek     private List<String> dirFileList;
158d8a7afe2SAdam Hornacek     private QueryBuilder queryBuilder;
159d8a7afe2SAdam Hornacek     private File dataRoot;
160d8a7afe2SAdam Hornacek     private StringBuilder headLines;
161d8a7afe2SAdam Hornacek     /**
162d8a7afe2SAdam Hornacek      * Page java scripts.
163d8a7afe2SAdam Hornacek      */
164d8a7afe2SAdam Hornacek     private final Scripts scripts = new Scripts();
165d8a7afe2SAdam Hornacek 
166d8a7afe2SAdam Hornacek     private static final String ATTR_NAME = PageConfig.class.getCanonicalName();
167d8a7afe2SAdam Hornacek     private HttpServletRequest req;
168d8a7afe2SAdam Hornacek 
169c6f0939bSAdam Hornacek     private final ExecutorService executor;
170d8a7afe2SAdam Hornacek 
171d8a7afe2SAdam Hornacek     /**
172d8a7afe2SAdam Hornacek      * Sets current request's attribute.
173d8a7afe2SAdam Hornacek      *
174d8a7afe2SAdam Hornacek      * @param attr attribute
175d8a7afe2SAdam Hornacek      * @param val value
176d8a7afe2SAdam Hornacek      */
setRequestAttribute(String attr, Object val)177d8a7afe2SAdam Hornacek     public void setRequestAttribute(String attr, Object val) {
178d8a7afe2SAdam Hornacek         this.req.setAttribute(attr, val);
179d8a7afe2SAdam Hornacek     }
180d8a7afe2SAdam Hornacek 
181d8a7afe2SAdam Hornacek     /**
182d8a7afe2SAdam Hornacek      * Gets current request's attribute.
183d8a7afe2SAdam Hornacek      * @param attr attribute
184d8a7afe2SAdam Hornacek      * @return Object attribute value or null if attribute does not exist
185d8a7afe2SAdam Hornacek      */
getRequestAttribute(String attr)186d8a7afe2SAdam Hornacek     public Object getRequestAttribute(String attr) {
187d8a7afe2SAdam Hornacek         return this.req.getAttribute(attr);
188d8a7afe2SAdam Hornacek     }
189d8a7afe2SAdam Hornacek 
190d8a7afe2SAdam Hornacek     /**
191d8a7afe2SAdam Hornacek      * Removes an attribute from the current request.
192d8a7afe2SAdam Hornacek      * @param string the attribute
193d8a7afe2SAdam Hornacek      */
removeAttribute(String string)194d8a7afe2SAdam Hornacek     public void removeAttribute(String string) {
195d8a7afe2SAdam Hornacek         req.removeAttribute(string);
196d8a7afe2SAdam Hornacek     }
197d8a7afe2SAdam Hornacek 
198d8a7afe2SAdam Hornacek     /**
199d8a7afe2SAdam Hornacek      * Add the given data to the &lt;head&gt; section of the html page to
200d8a7afe2SAdam Hornacek      * generate.
201d8a7afe2SAdam Hornacek      *
202d8a7afe2SAdam Hornacek      * @param data data to add. It is copied as is, so remember to escape
203d8a7afe2SAdam Hornacek      * special characters ...
204d8a7afe2SAdam Hornacek      */
addHeaderData(String data)205d8a7afe2SAdam Hornacek     public void addHeaderData(String data) {
206d8a7afe2SAdam Hornacek         if (data == null || data.length() == 0) {
207d8a7afe2SAdam Hornacek             return;
208d8a7afe2SAdam Hornacek         }
209d8a7afe2SAdam Hornacek         if (headLines == null) {
210d8a7afe2SAdam Hornacek             headLines = new StringBuilder();
211d8a7afe2SAdam Hornacek         }
212d8a7afe2SAdam Hornacek         headLines.append(data);
213d8a7afe2SAdam Hornacek     }
214d8a7afe2SAdam Hornacek 
215d8a7afe2SAdam Hornacek     /**
216d8a7afe2SAdam Hornacek      * Get addition data, which should be added as is to the &lt;head&gt;
217d8a7afe2SAdam Hornacek      * section of the html page.
218d8a7afe2SAdam Hornacek      *
219d8a7afe2SAdam Hornacek      * @return an empty string if nothing to add, the data otherwise.
220d8a7afe2SAdam Hornacek      */
getHeaderData()221d8a7afe2SAdam Hornacek     public String getHeaderData() {
222d8a7afe2SAdam Hornacek         return headLines == null ? "" : headLines.toString();
223d8a7afe2SAdam Hornacek     }
224d8a7afe2SAdam Hornacek 
225d8a7afe2SAdam Hornacek     /**
226379f387bSVladimir Kotal      * Extract file path and revision strings from the URL.
227379f387bSVladimir Kotal      * @param data DiffData object
228379f387bSVladimir Kotal      * @param context context path
229379f387bSVladimir Kotal      * @param filepath file path array (output parameter)
230379f387bSVladimir Kotal      * @return true if the extraction was successful, false otherwise
231379f387bSVladimir Kotal      * (in which case {@link DiffData#errorMsg} will be set)
232d8a7afe2SAdam Hornacek      */
getFileRevision(DiffData data, String context, String[] filepath)233379f387bSVladimir Kotal     private boolean getFileRevision(DiffData data, String context, String[] filepath) {
234d8a7afe2SAdam Hornacek         /*
235d8a7afe2SAdam Hornacek          * Basically the request URI looks like this:
236d8a7afe2SAdam Hornacek          * http://$site/$webapp/diff/$resourceFile?r1=$fileA@$revA&r2=$fileB@$revB
237d8a7afe2SAdam Hornacek          * The code below extracts file path and revision from the URI.
238d8a7afe2SAdam Hornacek          */
239d8a7afe2SAdam Hornacek         for (int i = 1; i <= 2; i++) {
240d8a7afe2SAdam Hornacek             String p = req.getParameter(QueryParameters.REVISION_PARAM + i);
241d8a7afe2SAdam Hornacek             if (p != null) {
242d8a7afe2SAdam Hornacek                 int j = p.lastIndexOf("@");
243d8a7afe2SAdam Hornacek                 if (j != -1) {
244d8a7afe2SAdam Hornacek                     filepath[i - 1] = p.substring(0, j);
245d8a7afe2SAdam Hornacek                     data.rev[i - 1] = p.substring(j + 1);
246d8a7afe2SAdam Hornacek                 }
247d8a7afe2SAdam Hornacek             }
248d8a7afe2SAdam Hornacek         }
249379f387bSVladimir Kotal 
250d8a7afe2SAdam Hornacek         if (data.rev[0] == null || data.rev[1] == null
251d8a7afe2SAdam Hornacek                 || data.rev[0].length() == 0 || data.rev[1].length() == 0
252d8a7afe2SAdam Hornacek                 || data.rev[0].equals(data.rev[1])) {
253d8a7afe2SAdam Hornacek             data.errorMsg = "Please pick two revisions to compare the changed "
254d8a7afe2SAdam Hornacek                     + "from the <a href=\"" + context + Prefix.HIST_L
255d8a7afe2SAdam Hornacek                     + getUriEncodedPath() + "\">history</a>";
256379f387bSVladimir Kotal             return false;
257379f387bSVladimir Kotal         }
258379f387bSVladimir Kotal 
259379f387bSVladimir Kotal         return true;
260379f387bSVladimir Kotal     }
261379f387bSVladimir Kotal 
262379f387bSVladimir Kotal     /**
263379f387bSVladimir Kotal      * Get all data required to create a diff view w.r.t. to this request in one go.
264379f387bSVladimir Kotal      *
265379f387bSVladimir Kotal      * @return an instance with just enough information to render a sufficient view.
266379f387bSVladimir Kotal      * If not all required parameters were given either they are supplemented
267379f387bSVladimir Kotal      * with reasonable defaults if possible, otherwise the related field(s) are {@code null}.
268379f387bSVladimir Kotal      * {@link DiffData#errorMsg}
269379f387bSVladimir Kotal      * {@code != null} indicates that an error occurred and one should not try to render a view.
270379f387bSVladimir Kotal      */
getDiffData()271379f387bSVladimir Kotal     public DiffData getDiffData() {
272379f387bSVladimir Kotal         DiffData data = new DiffData(getPath().substring(0, getPath().lastIndexOf(PATH_SEPARATOR)),
273379f387bSVladimir Kotal                 Util.htmlize(getResourceFile().getName()));
274379f387bSVladimir Kotal 
275379f387bSVladimir Kotal         String srcRoot = getSourceRootPath();
276379f387bSVladimir Kotal         String context = req.getContextPath();
277379f387bSVladimir Kotal         String[] filepath = new String[2];
278379f387bSVladimir Kotal 
279379f387bSVladimir Kotal         if (!getFileRevision(data, context, filepath)) {
280d8a7afe2SAdam Hornacek             return data;
281d8a7afe2SAdam Hornacek         }
282d8a7afe2SAdam Hornacek 
283379f387bSVladimir Kotal         data.genre = AnalyzerGuru.getGenre(getResourceFile().getName());
284d8a7afe2SAdam Hornacek         if (data.genre == null || txtGenres.contains(data.genre)) {
285d8a7afe2SAdam Hornacek             InputStream[] in = new InputStream[2];
286d8a7afe2SAdam Hornacek             try {
287d8a7afe2SAdam Hornacek                 // Get input stream for both older and newer file.
288d8a7afe2SAdam Hornacek                 Future<?>[] future = new Future<?>[2];
289d8a7afe2SAdam Hornacek                 for (int i = 0; i < 2; i++) {
290d8a7afe2SAdam Hornacek                     File f = new File(srcRoot + filepath[i]);
291d8a7afe2SAdam Hornacek                     final String revision = data.rev[i];
292d8a7afe2SAdam Hornacek                     future[i] = executor.submit(() -> HistoryGuru.getInstance().
293d8a7afe2SAdam Hornacek                             getRevision(f.getParent(), f.getName(), revision));
294d8a7afe2SAdam Hornacek                 }
295d8a7afe2SAdam Hornacek 
296d8a7afe2SAdam Hornacek                 for (int i = 0; i < 2; i++) {
297d8a7afe2SAdam Hornacek                     // The Executor used by given repository will enforce the timeout.
298d8a7afe2SAdam Hornacek                     in[i] = (InputStream) future[i].get();
299d8a7afe2SAdam Hornacek                     if (in[i] == null) {
300d8a7afe2SAdam Hornacek                         data.errorMsg = "Unable to get revision "
301d8a7afe2SAdam Hornacek                                 + Util.htmlize(data.rev[i]) + " for file: "
302d8a7afe2SAdam Hornacek                                 + Util.htmlize(getPath());
303d8a7afe2SAdam Hornacek                         return data;
304d8a7afe2SAdam Hornacek                     }
305d8a7afe2SAdam Hornacek                 }
306d8a7afe2SAdam Hornacek 
307d8a7afe2SAdam Hornacek                 /*
308d8a7afe2SAdam Hornacek                  * If the genre of the older revision cannot be determined,
309d8a7afe2SAdam Hornacek                  * (this can happen if the file was empty), try with newer
310d8a7afe2SAdam Hornacek                  * version.
311d8a7afe2SAdam Hornacek                  */
312d8a7afe2SAdam Hornacek                 for (int i = 0; i < 2 && data.genre == null; i++) {
313d8a7afe2SAdam Hornacek                     try {
314d8a7afe2SAdam Hornacek                         data.genre = AnalyzerGuru.getGenre(in[i]);
315d8a7afe2SAdam Hornacek                     } catch (IOException e) {
316d8a7afe2SAdam Hornacek                         data.errorMsg = "Unable to determine the file type: "
317d8a7afe2SAdam Hornacek                                 + Util.htmlize(e.getMessage());
318d8a7afe2SAdam Hornacek                     }
319d8a7afe2SAdam Hornacek                 }
320d8a7afe2SAdam Hornacek 
321d8a7afe2SAdam Hornacek                 if (data.genre != AbstractAnalyzer.Genre.PLAIN && data.genre != AbstractAnalyzer.Genre.HTML) {
322d8a7afe2SAdam Hornacek                     return data;
323d8a7afe2SAdam Hornacek                 }
324d8a7afe2SAdam Hornacek 
325d8a7afe2SAdam Hornacek                 ArrayList<String> lines = new ArrayList<>();
326d8a7afe2SAdam Hornacek                 Project p = getProject();
327d8a7afe2SAdam Hornacek                 for (int i = 0; i < 2; i++) {
328d8a7afe2SAdam Hornacek                     // All files under source root are read with UTF-8 as a default.
329d8a7afe2SAdam Hornacek                     try (BufferedReader br = new BufferedReader(
330d8a7afe2SAdam Hornacek                         ExpandTabsReader.wrap(IOUtils.createBOMStrippedReader(
331d8a7afe2SAdam Hornacek                         in[i], StandardCharsets.UTF_8.name()), p))) {
332d8a7afe2SAdam Hornacek                         String line;
333d8a7afe2SAdam Hornacek                         while ((line = br.readLine()) != null) {
334d8a7afe2SAdam Hornacek                             lines.add(line);
335d8a7afe2SAdam Hornacek                         }
336d8a7afe2SAdam Hornacek                         data.file[i] = lines.toArray(new String[0]);
337d8a7afe2SAdam Hornacek                         lines.clear();
338d8a7afe2SAdam Hornacek                     }
339d8a7afe2SAdam Hornacek                     in[i] = null;
340d8a7afe2SAdam Hornacek                 }
341d8a7afe2SAdam Hornacek             } catch (Exception e) {
342d8a7afe2SAdam Hornacek                 data.errorMsg = "Error reading revisions: "
343d8a7afe2SAdam Hornacek                         + Util.htmlize(e.getMessage());
344d8a7afe2SAdam Hornacek             } finally {
345d8a7afe2SAdam Hornacek                 for (int i = 0; i < 2; i++) {
346d8a7afe2SAdam Hornacek                     IOUtils.close(in[i]);
347d8a7afe2SAdam Hornacek                 }
348d8a7afe2SAdam Hornacek             }
349d8a7afe2SAdam Hornacek             if (data.errorMsg != null) {
350d8a7afe2SAdam Hornacek                 return data;
351d8a7afe2SAdam Hornacek             }
352d8a7afe2SAdam Hornacek             try {
353d8a7afe2SAdam Hornacek                 data.revision = Diff.diff(data.file[0], data.file[1]);
354d8a7afe2SAdam Hornacek             } catch (DifferentiationFailedException e) {
355379f387bSVladimir Kotal                 data.errorMsg = "Unable to get diffs: " + Util.htmlize(e.getMessage());
356d8a7afe2SAdam Hornacek             }
357d8a7afe2SAdam Hornacek             for (int i = 0; i < 2; i++) {
358d8a7afe2SAdam Hornacek                 try {
359d8a7afe2SAdam Hornacek                     URI u = new URI(null, null, null,
360d8a7afe2SAdam Hornacek                             filepath[i] + "@" + data.rev[i], null);
361d8a7afe2SAdam Hornacek                     data.param[i] = u.getRawQuery();
362d8a7afe2SAdam Hornacek                 } catch (URISyntaxException e) {
363d8a7afe2SAdam Hornacek                     LOGGER.log(Level.WARNING, "Failed to create URI: ", e);
364d8a7afe2SAdam Hornacek                 }
365d8a7afe2SAdam Hornacek             }
366d8a7afe2SAdam Hornacek             data.full = fullDiff();
367d8a7afe2SAdam Hornacek             data.type = getDiffType();
368d8a7afe2SAdam Hornacek         }
369d8a7afe2SAdam Hornacek         return data;
370d8a7afe2SAdam Hornacek     }
371d8a7afe2SAdam Hornacek 
372d8a7afe2SAdam Hornacek     /**
373d8a7afe2SAdam Hornacek      * Get the diff display type to use wrt. the request parameter
374d8a7afe2SAdam Hornacek      * {@code format}.
375d8a7afe2SAdam Hornacek      *
376d8a7afe2SAdam Hornacek      * @return {@link DiffType#SIDEBYSIDE} if the request contains no such
377d8a7afe2SAdam Hornacek      * parameter or one with an unknown value, the recognized diff type
378d8a7afe2SAdam Hornacek      * otherwise.
379d8a7afe2SAdam Hornacek      * @see DiffType#get(String)
380d8a7afe2SAdam Hornacek      * @see DiffType#getAbbrev()
381d8a7afe2SAdam Hornacek      * @see DiffType#toString()
382d8a7afe2SAdam Hornacek      */
getDiffType()383d8a7afe2SAdam Hornacek     public DiffType getDiffType() {
384d8a7afe2SAdam Hornacek         DiffType d = DiffType.get(req.getParameter(QueryParameters.FORMAT_PARAM));
385d8a7afe2SAdam Hornacek         return d == null ? DiffType.SIDEBYSIDE : d;
386d8a7afe2SAdam Hornacek     }
387d8a7afe2SAdam Hornacek 
388d8a7afe2SAdam Hornacek     /**
389d8a7afe2SAdam Hornacek      * Check, whether a full diff should be displayed.
390d8a7afe2SAdam Hornacek      *
391d8a7afe2SAdam Hornacek      * @return {@code true} if a request parameter {@code full} with the literal
392d8a7afe2SAdam Hornacek      * value {@code 1} was found.
393d8a7afe2SAdam Hornacek      */
fullDiff()394d8a7afe2SAdam Hornacek     public boolean fullDiff() {
395d8a7afe2SAdam Hornacek         String val = req.getParameter(QueryParameters.DIFF_LEVEL_PARAM);
396d8a7afe2SAdam Hornacek         return val != null && val.equals("1");
397d8a7afe2SAdam Hornacek     }
398d8a7afe2SAdam Hornacek 
399d8a7afe2SAdam Hornacek     /**
400d8a7afe2SAdam Hornacek      * Check, whether the request contains minimal information required to
401d8a7afe2SAdam Hornacek      * produce a valid page. If this method returns an empty string, the
402d8a7afe2SAdam Hornacek      * referred file or directory actually exists below the source root
403d8a7afe2SAdam Hornacek      * directory and is readable.
404d8a7afe2SAdam Hornacek      *
405d8a7afe2SAdam Hornacek      * @return {@code null} if the referred src file, directory or history is
406d8a7afe2SAdam Hornacek      * not available, an empty String if further processing is ok and a
407d8a7afe2SAdam Hornacek      * non-empty string which contains the URI encoded redirect path if the
408d8a7afe2SAdam Hornacek      * request should be redirected.
409d8a7afe2SAdam Hornacek      * @see #resourceNotAvailable()
410d8a7afe2SAdam Hornacek      * @see #getDirectoryRedirect()
411d8a7afe2SAdam Hornacek      */
canProcess()412d8a7afe2SAdam Hornacek     public String canProcess() {
413d8a7afe2SAdam Hornacek         if (resourceNotAvailable()) {
414d8a7afe2SAdam Hornacek             return null;
415d8a7afe2SAdam Hornacek         }
416d8a7afe2SAdam Hornacek         String redir = getDirectoryRedirect();
417d8a7afe2SAdam Hornacek         if (redir == null && getPrefix() == Prefix.HIST_L && !hasHistory()) {
418d8a7afe2SAdam Hornacek             return null;
419d8a7afe2SAdam Hornacek         }
420d8a7afe2SAdam Hornacek         // jel: outfactored from list.jsp - seems to be bogus
421d8a7afe2SAdam Hornacek         if (isDir()) {
422d8a7afe2SAdam Hornacek             if (getPrefix() == Prefix.XREF_P) {
423d8a7afe2SAdam Hornacek                 if (getResourceFileList().isEmpty()
424d8a7afe2SAdam Hornacek                         && !getRequestedRevision().isEmpty() && !hasHistory()) {
425d8a7afe2SAdam Hornacek                     return null;
426d8a7afe2SAdam Hornacek                 }
427d8a7afe2SAdam Hornacek             } else if ((getPrefix() == Prefix.RAW_P)
428d8a7afe2SAdam Hornacek                     || (getPrefix() == Prefix.DOWNLOAD_P)) {
429d8a7afe2SAdam Hornacek                 return null;
430d8a7afe2SAdam Hornacek             }
431d8a7afe2SAdam Hornacek         }
432d8a7afe2SAdam Hornacek         return redir == null ? "" : redir;
433d8a7afe2SAdam Hornacek     }
434d8a7afe2SAdam Hornacek 
435d8a7afe2SAdam Hornacek     /**
436d8a7afe2SAdam Hornacek      * Get a list of filenames in the requested path.
437d8a7afe2SAdam Hornacek      *
438d8a7afe2SAdam Hornacek      * @return an empty list, if the resource does not exist, is not a directory
439d8a7afe2SAdam Hornacek      * or an error occurred when reading it, otherwise a list of filenames in
440d8a7afe2SAdam Hornacek      * that directory, sorted alphabetically
441d8a7afe2SAdam Hornacek      *
442d8a7afe2SAdam Hornacek      * <p>
443d8a7afe2SAdam Hornacek      * For the root directory (/xref/) an authorization is performed for each
4444a04c503SChris Fraire      * project in case that projects are used.
445d8a7afe2SAdam Hornacek      *
446d8a7afe2SAdam Hornacek      * @see #getResourceFile()
447d8a7afe2SAdam Hornacek      * @see #isDir()
448d8a7afe2SAdam Hornacek      */
getResourceFileList()449d8a7afe2SAdam Hornacek     public List<String> getResourceFileList() {
450d8a7afe2SAdam Hornacek         if (dirFileList == null) {
451d8a7afe2SAdam Hornacek             File[] files = null;
452d8a7afe2SAdam Hornacek             if (isDir() && getResourcePath().length() > 1) {
453d8a7afe2SAdam Hornacek                 files = getResourceFile().listFiles();
454d8a7afe2SAdam Hornacek             }
45534c930beSVladimir Kotal 
456d8a7afe2SAdam Hornacek             if (files == null) {
457d8a7afe2SAdam Hornacek                 dirFileList = Collections.emptyList();
458d8a7afe2SAdam Hornacek             } else {
45934c930beSVladimir Kotal                 List<String> listOfFiles = getSortedFiles(files);
460d8a7afe2SAdam Hornacek 
461d8a7afe2SAdam Hornacek                 if (env.hasProjects() && getPath().isEmpty()) {
462379f387bSVladimir Kotal                     /*
463d8a7afe2SAdam Hornacek                      * This denotes the source root directory, we need to filter
464d8a7afe2SAdam Hornacek                      * projects which aren't allowed by the authorization
465d8a7afe2SAdam Hornacek                      * because otherwise the main xref page expose the names of
466d8a7afe2SAdam Hornacek                      * all projects in OpenGrok even those which aren't allowed
467d8a7afe2SAdam Hornacek                      * for the particular user. E.g. remove all which aren't
468d8a7afe2SAdam Hornacek                      * among the filtered set of projects.
469d8a7afe2SAdam Hornacek                      *
470d8a7afe2SAdam Hornacek                      * The authorization check is made in
471d8a7afe2SAdam Hornacek                      * {@link ProjectHelper#getAllProjects()} as a part of all
472d8a7afe2SAdam Hornacek                      * projects filtering.
473d8a7afe2SAdam Hornacek                      */
474d8a7afe2SAdam Hornacek                     List<String> modifiableListOfFiles = new ArrayList<>(listOfFiles);
475c6f0939bSAdam Hornacek                     modifiableListOfFiles.removeIf(t -> getProjectHelper().getAllProjects().stream()
476c6f0939bSAdam Hornacek                             .noneMatch(p -> p.getName().equalsIgnoreCase(t)));
477d8a7afe2SAdam Hornacek                     return dirFileList = Collections.unmodifiableList(modifiableListOfFiles);
478d8a7afe2SAdam Hornacek                 }
479d8a7afe2SAdam Hornacek 
480d8a7afe2SAdam Hornacek                 dirFileList = Collections.unmodifiableList(listOfFiles);
481d8a7afe2SAdam Hornacek             }
482d8a7afe2SAdam Hornacek         }
483d8a7afe2SAdam Hornacek         return dirFileList;
484d8a7afe2SAdam Hornacek     }
485d8a7afe2SAdam Hornacek 
getFileComparator()48634c930beSVladimir Kotal     private Comparator<File> getFileComparator() {
48734c930beSVladimir Kotal         if (getEnv().getListDirsFirst()) {
48834c930beSVladimir Kotal             return (f1, f2) -> {
48934c930beSVladimir Kotal                 if (f1.isDirectory() && !f2.isDirectory()) {
49034c930beSVladimir Kotal                     return -1;
49134c930beSVladimir Kotal                 } else if (!f1.isDirectory() && f2.isDirectory()) {
49234c930beSVladimir Kotal                     return 1;
49334c930beSVladimir Kotal                 } else {
49434c930beSVladimir Kotal                     return f1.getName().compareTo(f2.getName());
49534c930beSVladimir Kotal                 }
49634c930beSVladimir Kotal             };
49734c930beSVladimir Kotal         } else {
49834c930beSVladimir Kotal             return Comparator.comparing(File::getName);
49934c930beSVladimir Kotal         }
50034c930beSVladimir Kotal     }
50134c930beSVladimir Kotal 
50234c930beSVladimir Kotal     @VisibleForTesting
50334c930beSVladimir Kotal     List<String> getSortedFiles(File[] files) {
50434c930beSVladimir Kotal         return Arrays.stream(files).sorted(getFileComparator()).map(File::getName).collect(Collectors.toList());
50534c930beSVladimir Kotal     }
50634c930beSVladimir Kotal 
507d8a7afe2SAdam Hornacek     /**
508d8a7afe2SAdam Hornacek      * Get the time of last modification of the related file or directory.
509d8a7afe2SAdam Hornacek      *
510d8a7afe2SAdam Hornacek      * @return the last modification time of the related file or directory.
511d8a7afe2SAdam Hornacek      * @see File#lastModified()
512d8a7afe2SAdam Hornacek      */
513d8a7afe2SAdam Hornacek     public long getLastModified() {
514d8a7afe2SAdam Hornacek         return getResourceFile().lastModified();
515d8a7afe2SAdam Hornacek     }
516d8a7afe2SAdam Hornacek 
517d8a7afe2SAdam Hornacek     /**
518d8a7afe2SAdam Hornacek      * Get all RSS related directories from the request using its {@code also}
519d8a7afe2SAdam Hornacek      * parameter.
520d8a7afe2SAdam Hornacek      *
521d8a7afe2SAdam Hornacek      * @return an empty string if the requested resource is not a directory, a
522d8a7afe2SAdam Hornacek      * space (' ') separated list of unchecked directory names otherwise.
523d8a7afe2SAdam Hornacek      */
524d8a7afe2SAdam Hornacek     public String getHistoryDirs() {
525d8a7afe2SAdam Hornacek         if (!isDir()) {
526d8a7afe2SAdam Hornacek             return "";
527d8a7afe2SAdam Hornacek         }
528d8a7afe2SAdam Hornacek         String[] val = req.getParameterValues("also");
529d8a7afe2SAdam Hornacek         if (val == null || val.length == 0) {
530d8a7afe2SAdam Hornacek             return getPath();
531d8a7afe2SAdam Hornacek         }
532d8a7afe2SAdam Hornacek         StringBuilder paths = new StringBuilder(getPath());
533d8a7afe2SAdam Hornacek         for (String val1 : val) {
534d8a7afe2SAdam Hornacek             paths.append(' ').append(val1);
535d8a7afe2SAdam Hornacek         }
536d8a7afe2SAdam Hornacek         return paths.toString();
537d8a7afe2SAdam Hornacek     }
538d8a7afe2SAdam Hornacek 
539d8a7afe2SAdam Hornacek     /**
540d8a7afe2SAdam Hornacek      * Get the int value of the given request parameter.
541d8a7afe2SAdam Hornacek      *
542d8a7afe2SAdam Hornacek      * @param name name of the parameter to lookup.
543d8a7afe2SAdam Hornacek      * @param defaultValue value to return, if the parameter is not set, is not
544d8a7afe2SAdam Hornacek      * a number, or is &lt; 0.
545d8a7afe2SAdam Hornacek      * @return the parsed int value on success, the given default value
546d8a7afe2SAdam Hornacek      * otherwise.
547d8a7afe2SAdam Hornacek      */
548d8a7afe2SAdam Hornacek     public int getIntParam(String name, int defaultValue) {
549d8a7afe2SAdam Hornacek         int ret = defaultValue;
550d8a7afe2SAdam Hornacek         String s = req.getParameter(name);
551d8a7afe2SAdam Hornacek         if (s != null && s.length() != 0) {
552d8a7afe2SAdam Hornacek             try {
553d8a7afe2SAdam Hornacek                 int x = Integer.parseInt(s, 10);
554d8a7afe2SAdam Hornacek                 if (x >= 0) {
555d8a7afe2SAdam Hornacek                     ret = x;
556d8a7afe2SAdam Hornacek                 }
557d8a7afe2SAdam Hornacek             } catch (NumberFormatException e) {
558379f387bSVladimir Kotal                 LOGGER.log(Level.INFO, String.format("Failed to parse %s integer %s", name, s), e);
559d8a7afe2SAdam Hornacek             }
560d8a7afe2SAdam Hornacek         }
561d8a7afe2SAdam Hornacek         return ret;
562d8a7afe2SAdam Hornacek     }
563d8a7afe2SAdam Hornacek 
564d8a7afe2SAdam Hornacek     /**
565d8a7afe2SAdam Hornacek      * Get the <b>start</b> index for a search result to return by looking up
566d8a7afe2SAdam Hornacek      * the {@code start} request parameter.
567d8a7afe2SAdam Hornacek      *
568d8a7afe2SAdam Hornacek      * @return 0 if the corresponding start parameter is not set or not a
569d8a7afe2SAdam Hornacek      * number, the number found otherwise.
570d8a7afe2SAdam Hornacek      */
571d8a7afe2SAdam Hornacek     public int getSearchStart() {
572d8a7afe2SAdam Hornacek         return getIntParam(QueryParameters.START_PARAM, 0);
573d8a7afe2SAdam Hornacek     }
574d8a7afe2SAdam Hornacek 
575d8a7afe2SAdam Hornacek     /**
576d8a7afe2SAdam Hornacek      * Get the number of search results to max. return by looking up the
577d8a7afe2SAdam Hornacek      * {@code n} request parameter.
578d8a7afe2SAdam Hornacek      *
579d8a7afe2SAdam Hornacek      * @return the default number of hits if the corresponding start parameter
580d8a7afe2SAdam Hornacek      * is not set or not a number, the number found otherwise.
581d8a7afe2SAdam Hornacek      */
582d8a7afe2SAdam Hornacek     public int getSearchMaxItems() {
583d8a7afe2SAdam Hornacek         return getIntParam(QueryParameters.COUNT_PARAM, getEnv().getHitsPerPage());
584d8a7afe2SAdam Hornacek     }
585d8a7afe2SAdam Hornacek 
586d8a7afe2SAdam Hornacek     public int getRevisionMessageCollapseThreshold() {
587d8a7afe2SAdam Hornacek         return getEnv().getRevisionMessageCollapseThreshold();
588d8a7afe2SAdam Hornacek     }
589d8a7afe2SAdam Hornacek 
590d8a7afe2SAdam Hornacek     public int getCurrentIndexedCollapseThreshold() {
591d8a7afe2SAdam Hornacek         return getEnv().getCurrentIndexedCollapseThreshold();
592d8a7afe2SAdam Hornacek     }
593d8a7afe2SAdam Hornacek 
594d8a7afe2SAdam Hornacek     public int getGroupsCollapseThreshold() {
595d8a7afe2SAdam Hornacek         return getEnv().getGroupsCollapseThreshold();
596d8a7afe2SAdam Hornacek     }
597d8a7afe2SAdam Hornacek 
598d8a7afe2SAdam Hornacek     /**
599d8a7afe2SAdam Hornacek      * Get sort orders from the request parameter {@code sort} and if this list
600379f387bSVladimir Kotal      * would be empty from the cookie {@code OpenGrokSorting}.
601d8a7afe2SAdam Hornacek      *
602d8a7afe2SAdam Hornacek      * @return a possible empty list which contains the sort order values in the
603d8a7afe2SAdam Hornacek      * same order supplied by the request parameter or cookie(s).
604d8a7afe2SAdam Hornacek      */
605d8a7afe2SAdam Hornacek     public List<SortOrder> getSortOrder() {
606d8a7afe2SAdam Hornacek         List<SortOrder> sort = new ArrayList<>();
607379f387bSVladimir Kotal         List<String> vals = getParameterValues(QueryParameters.SORT_PARAM);
608d8a7afe2SAdam Hornacek         for (String s : vals) {
609d8a7afe2SAdam Hornacek             SortOrder so = SortOrder.get(s);
610d8a7afe2SAdam Hornacek             if (so != null) {
611d8a7afe2SAdam Hornacek                 sort.add(so);
612d8a7afe2SAdam Hornacek             }
613d8a7afe2SAdam Hornacek         }
614d8a7afe2SAdam Hornacek         if (sort.isEmpty()) {
615d8a7afe2SAdam Hornacek             vals = getCookieVals("OpenGrokSorting");
616d8a7afe2SAdam Hornacek             for (String s : vals) {
617d8a7afe2SAdam Hornacek                 SortOrder so = SortOrder.get(s);
618d8a7afe2SAdam Hornacek                 if (so != null) {
619d8a7afe2SAdam Hornacek                     sort.add(so);
620d8a7afe2SAdam Hornacek                 }
621d8a7afe2SAdam Hornacek             }
622d8a7afe2SAdam Hornacek         }
623d8a7afe2SAdam Hornacek         return sort;
624d8a7afe2SAdam Hornacek     }
625d8a7afe2SAdam Hornacek 
626d8a7afe2SAdam Hornacek     /**
627d8a7afe2SAdam Hornacek      * Get a reference to the {@code QueryBuilder} wrt. to the current request
628d8a7afe2SAdam Hornacek      * parameters: <dl> <dt>q</dt> <dd>freetext lookup rules</dd> <dt>defs</dt>
629d8a7afe2SAdam Hornacek      * <dd>definitions lookup rules</dd> <dt>path</dt> <dd>path related
630d8a7afe2SAdam Hornacek      * rules</dd> <dt>hist</dt> <dd>history related rules</dd> </dl>
631d8a7afe2SAdam Hornacek      *
632d8a7afe2SAdam Hornacek      * @return a query builder with all relevant fields populated.
633d8a7afe2SAdam Hornacek      */
634d8a7afe2SAdam Hornacek     public QueryBuilder getQueryBuilder() {
635d8a7afe2SAdam Hornacek         if (queryBuilder == null) {
636d8a7afe2SAdam Hornacek             queryBuilder = new QueryBuilder().
637d8a7afe2SAdam Hornacek                     setFreetext(Laundromat.launderQuery(req.getParameter(QueryBuilder.FULL)))
638d8a7afe2SAdam Hornacek                     .setDefs(Laundromat.launderQuery(req.getParameter(QueryBuilder.DEFS)))
639d8a7afe2SAdam Hornacek                     .setRefs(Laundromat.launderQuery(req.getParameter(QueryBuilder.REFS)))
640d8a7afe2SAdam Hornacek                     .setPath(Laundromat.launderQuery(req.getParameter(QueryBuilder.PATH)))
641d8a7afe2SAdam Hornacek                     .setHist(Laundromat.launderQuery(req.getParameter(QueryBuilder.HIST)))
642d8a7afe2SAdam Hornacek                     .setType(Laundromat.launderQuery(req.getParameter(QueryBuilder.TYPE)));
643d8a7afe2SAdam Hornacek         }
644d8a7afe2SAdam Hornacek 
645d8a7afe2SAdam Hornacek         return queryBuilder;
646d8a7afe2SAdam Hornacek     }
647d8a7afe2SAdam Hornacek 
648d8a7afe2SAdam Hornacek     /**
649379f387bSVladimir Kotal      * Get the <i>Eftar</i> reader for the data directory. If it has been already
650d8a7afe2SAdam Hornacek      * opened and not closed, this instance gets returned. One should not close
651d8a7afe2SAdam Hornacek      * it once used: {@link #cleanup(ServletRequest)} takes care to close it.
652d8a7afe2SAdam Hornacek      *
653d8a7afe2SAdam Hornacek      * @return {@code null} if a reader can't be established, the reader
654d8a7afe2SAdam Hornacek      * otherwise.
655d8a7afe2SAdam Hornacek      */
656d8a7afe2SAdam Hornacek     public EftarFileReader getEftarReader() {
657d8a7afe2SAdam Hornacek         if (eftarReader == null || eftarReader.isClosed()) {
658d8a7afe2SAdam Hornacek             File f = getEnv().getDtagsEftar();
659d8a7afe2SAdam Hornacek             if (f == null) {
660d8a7afe2SAdam Hornacek                 eftarReader = null;
661d8a7afe2SAdam Hornacek             } else {
662d8a7afe2SAdam Hornacek                 try {
663d8a7afe2SAdam Hornacek                     eftarReader = new EftarFileReader(f);
664d8a7afe2SAdam Hornacek                 } catch (FileNotFoundException e) {
665d8a7afe2SAdam Hornacek                     LOGGER.log(Level.FINE, "Failed to create EftarFileReader: ", e);
666d8a7afe2SAdam Hornacek                 }
667d8a7afe2SAdam Hornacek             }
668d8a7afe2SAdam Hornacek         }
669d8a7afe2SAdam Hornacek         return eftarReader;
670d8a7afe2SAdam Hornacek     }
671d8a7afe2SAdam Hornacek 
672d8a7afe2SAdam Hornacek     /**
673d8a7afe2SAdam Hornacek      * Get the definition tag for the request related file or directory.
674d8a7afe2SAdam Hornacek      *
675d8a7afe2SAdam Hornacek      * @return an empty string if not found, the tag otherwise.
676d8a7afe2SAdam Hornacek      */
677d8a7afe2SAdam Hornacek     public String getDefineTagsIndex() {
678d8a7afe2SAdam Hornacek         if (dtag != null) {
679d8a7afe2SAdam Hornacek             return dtag;
680d8a7afe2SAdam Hornacek         }
681d8a7afe2SAdam Hornacek         getEftarReader();
682d8a7afe2SAdam Hornacek         if (eftarReader != null) {
683d8a7afe2SAdam Hornacek             try {
684d8a7afe2SAdam Hornacek                 dtag = eftarReader.get(getPath());
685d8a7afe2SAdam Hornacek             } catch (IOException e) {
686d8a7afe2SAdam Hornacek                 LOGGER.log(Level.INFO, "Failed to get entry from eftar reader: ", e);
687d8a7afe2SAdam Hornacek             }
688d8a7afe2SAdam Hornacek         }
689d8a7afe2SAdam Hornacek         if (dtag == null) {
690d8a7afe2SAdam Hornacek             dtag = "";
691d8a7afe2SAdam Hornacek         }
692d8a7afe2SAdam Hornacek         return dtag;
693d8a7afe2SAdam Hornacek     }
694d8a7afe2SAdam Hornacek 
695d8a7afe2SAdam Hornacek     /**
696d8a7afe2SAdam Hornacek      * Get the revision parameter {@code r} from the request.
697d8a7afe2SAdam Hornacek      *
698d8a7afe2SAdam Hornacek      * @return revision if found, an empty string otherwise.
699d8a7afe2SAdam Hornacek      */
700d8a7afe2SAdam Hornacek     public String getRequestedRevision() {
701d8a7afe2SAdam Hornacek         if (rev == null) {
702d8a7afe2SAdam Hornacek             String tmp = Laundromat.launderInput(
703d8a7afe2SAdam Hornacek                     req.getParameter(QueryParameters.REVISION_PARAM));
704d8a7afe2SAdam Hornacek             rev = (tmp != null && tmp.length() > 0) ? tmp : "";
705d8a7afe2SAdam Hornacek         }
706d8a7afe2SAdam Hornacek         return rev;
707d8a7afe2SAdam Hornacek     }
708d8a7afe2SAdam Hornacek 
709d8a7afe2SAdam Hornacek     /**
710d8a7afe2SAdam Hornacek      * Check, whether the request related resource has history information.
711d8a7afe2SAdam Hornacek      *
712d8a7afe2SAdam Hornacek      * @return {@code true} if history is available.
713d8a7afe2SAdam Hornacek      * @see HistoryGuru#hasHistory(File)
714d8a7afe2SAdam Hornacek      */
715d8a7afe2SAdam Hornacek     public boolean hasHistory() {
716d8a7afe2SAdam Hornacek         if (hasHistory == null) {
717d8a7afe2SAdam Hornacek             hasHistory = HistoryGuru.getInstance().hasHistory(getResourceFile());
718d8a7afe2SAdam Hornacek         }
719d8a7afe2SAdam Hornacek         return hasHistory;
720d8a7afe2SAdam Hornacek     }
721d8a7afe2SAdam Hornacek 
722d8a7afe2SAdam Hornacek     /**
723d8a7afe2SAdam Hornacek      * Check, whether annotations are available for the related resource.
724d8a7afe2SAdam Hornacek      *
725d8a7afe2SAdam Hornacek      * @return {@code true} if annotations are available.
726d8a7afe2SAdam Hornacek      */
727d8a7afe2SAdam Hornacek     public boolean hasAnnotations() {
728d8a7afe2SAdam Hornacek         if (hasAnnotation == null) {
729d8a7afe2SAdam Hornacek             hasAnnotation = !isDir()
730d8a7afe2SAdam Hornacek                     && HistoryGuru.getInstance().hasHistory(getResourceFile());
731d8a7afe2SAdam Hornacek         }
732d8a7afe2SAdam Hornacek         return hasAnnotation;
733d8a7afe2SAdam Hornacek     }
734d8a7afe2SAdam Hornacek 
735d8a7afe2SAdam Hornacek     /**
736d8a7afe2SAdam Hornacek      * Check, whether the resource to show should be annotated.
737d8a7afe2SAdam Hornacek      *
738d8a7afe2SAdam Hornacek      * @return {@code true} if annotation is desired and available.
739d8a7afe2SAdam Hornacek      */
740d8a7afe2SAdam Hornacek     public boolean annotate() {
741d8a7afe2SAdam Hornacek         if (annotate == null) {
742d8a7afe2SAdam Hornacek             annotate = hasAnnotations()
743d8a7afe2SAdam Hornacek                     && Boolean.parseBoolean(req.getParameter(QueryParameters.ANNOTATION_PARAM));
744d8a7afe2SAdam Hornacek         }
745d8a7afe2SAdam Hornacek         return annotate;
746d8a7afe2SAdam Hornacek     }
747d8a7afe2SAdam Hornacek 
748d8a7afe2SAdam Hornacek     /**
749d8a7afe2SAdam Hornacek      * Get the annotation for the requested resource.
750d8a7afe2SAdam Hornacek      *
751d8a7afe2SAdam Hornacek      * @return {@code null} if not available or annotation was not requested,
752d8a7afe2SAdam Hornacek      * the cached annotation otherwise.
753d8a7afe2SAdam Hornacek      */
754d8a7afe2SAdam Hornacek     public Annotation getAnnotation() {
755d8a7afe2SAdam Hornacek         if (isDir() || getResourcePath().equals("/") || !annotate()) {
756d8a7afe2SAdam Hornacek             return null;
757d8a7afe2SAdam Hornacek         }
758d8a7afe2SAdam Hornacek         if (annotation != null) {
759d8a7afe2SAdam Hornacek             return annotation;
760d8a7afe2SAdam Hornacek         }
761d8a7afe2SAdam Hornacek         getRequestedRevision();
762d8a7afe2SAdam Hornacek         try {
763d8a7afe2SAdam Hornacek             annotation = HistoryGuru.getInstance().annotate(resourceFile, rev.isEmpty() ? null : rev);
764d8a7afe2SAdam Hornacek         } catch (IOException e) {
765d8a7afe2SAdam Hornacek             LOGGER.log(Level.WARNING, "Failed to get annotations: ", e);
766d8a7afe2SAdam Hornacek             /* ignore */
767d8a7afe2SAdam Hornacek         }
768d8a7afe2SAdam Hornacek         return annotation;
769d8a7afe2SAdam Hornacek     }
770d8a7afe2SAdam Hornacek 
771d8a7afe2SAdam Hornacek     /**
772379f387bSVladimir Kotal      * Get the {@code path} parameter and display value for "Search only in" option.
773d8a7afe2SAdam Hornacek      *
774d8a7afe2SAdam Hornacek      * @return always an array of 3 fields, whereby field[0] contains the path
775379f387bSVladimir Kotal      * value to use (starts and ends always with a {@link org.opengrok.indexer.index.Indexer#PATH_SEPARATOR}).
776379f387bSVladimir Kotal      * Field[1] the contains string to show in the UI. field[2] is set to {@code disabled=""} if the
777d8a7afe2SAdam Hornacek      * current path is the "/" directory, otherwise set to an empty string.
778d8a7afe2SAdam Hornacek      */
779d8a7afe2SAdam Hornacek     public String[] getSearchOnlyIn() {
780d8a7afe2SAdam Hornacek         if (isDir()) {
781d8a7afe2SAdam Hornacek             return getPath().length() == 0
782d8a7afe2SAdam Hornacek                     ? new String[]{"/", "this directory", "disabled=\"\""}
783d8a7afe2SAdam Hornacek                     : new String[]{getPath(), "this directory", ""};
784d8a7afe2SAdam Hornacek         }
785d8a7afe2SAdam Hornacek         String[] res = new String[3];
786d8a7afe2SAdam Hornacek         res[0] = getPath().substring(0, getPath().lastIndexOf(PATH_SEPARATOR) + 1);
787d8a7afe2SAdam Hornacek         res[1] = res[0];
788d8a7afe2SAdam Hornacek         res[2] = "";
789d8a7afe2SAdam Hornacek         return res;
790d8a7afe2SAdam Hornacek     }
791d8a7afe2SAdam Hornacek 
792d8a7afe2SAdam Hornacek     /**
793d8a7afe2SAdam Hornacek      * Get the project {@link #getPath()} refers to.
794d8a7afe2SAdam Hornacek      *
795d8a7afe2SAdam Hornacek      * @return {@code null} if not available, the project otherwise.
796d8a7afe2SAdam Hornacek      */
79764819763SVladimir Kotal     @Nullable
798d8a7afe2SAdam Hornacek     public Project getProject() {
799d8a7afe2SAdam Hornacek         return Project.getProject(getResourceFile());
800d8a7afe2SAdam Hornacek     }
801d8a7afe2SAdam Hornacek 
802d8a7afe2SAdam Hornacek     /**
803d8a7afe2SAdam Hornacek      * Same as {@link #getRequestedProjects()} but returns the project names as
804d8a7afe2SAdam Hornacek      * a coma separated String.
805d8a7afe2SAdam Hornacek      *
806d8a7afe2SAdam Hornacek      * @return a possible empty String but never {@code null}.
807d8a7afe2SAdam Hornacek      */
808d8a7afe2SAdam Hornacek     public String getRequestedProjectsAsString() {
809d8a7afe2SAdam Hornacek         if (requestedProjectsString == null) {
810912cbfdfSAdam Hornacek             requestedProjectsString = String.join(",", getRequestedProjects());
811d8a7afe2SAdam Hornacek         }
812d8a7afe2SAdam Hornacek         return requestedProjectsString;
813d8a7afe2SAdam Hornacek     }
814d8a7afe2SAdam Hornacek 
815d8a7afe2SAdam Hornacek     /**
816d8a7afe2SAdam Hornacek      * Get a reference to a set of requested projects via request parameter
817d8a7afe2SAdam Hornacek      * {@code project} or cookies or defaults.
818d8a7afe2SAdam Hornacek      * <p>
819d8a7afe2SAdam Hornacek      * NOTE: This method assumes, that project names do <b>not</b> contain a
820d8a7afe2SAdam Hornacek      * comma (','), since this character is used as name separator!
821d8a7afe2SAdam Hornacek      * <p>
822d8a7afe2SAdam Hornacek      * It is determined as follows:
823d8a7afe2SAdam Hornacek      * <ol>
824d8a7afe2SAdam Hornacek      * <li>If there is no project in the configuration an empty set is returned. Otherwise:</li>
825d8a7afe2SAdam Hornacek      * <li>If there is only one project in the configuration,
826d8a7afe2SAdam Hornacek      * this one gets returned (no matter, what the request actually says). Otherwise</li>
827d8a7afe2SAdam Hornacek      * <li>If the request parameter {@code ALL_PROJECT_SEARCH} contains a true value,
828d8a7afe2SAdam Hornacek      * all projects are added to searching. Otherwise:</li>
829d8a7afe2SAdam Hornacek      * <li>If the request parameter {@code PROJECT_PARAM_NAME} contains any available project,
830d8a7afe2SAdam Hornacek      * the set with invalid projects removed gets returned. Otherwise:</li>
831d8a7afe2SAdam Hornacek      * <li>If the request parameter {@code GROUP_PARAM_NAME} contains any available group,
832d8a7afe2SAdam Hornacek      * then all projects from that group will be added to the result set. Otherwise:</li>
833d8a7afe2SAdam Hornacek      * <li>If the request has a cookie with the name {@code OPEN_GROK_PROJECT}
834d8a7afe2SAdam Hornacek      * and it contains any available project,
835d8a7afe2SAdam Hornacek      * the set with invalid projects removed gets returned. Otherwise:</li>
836d8a7afe2SAdam Hornacek      * <li>If a default project is set in the configuration,
837d8a7afe2SAdam Hornacek      * this project gets returned. Otherwise:</li>
838d8a7afe2SAdam Hornacek      * <li>an empty set</li>
839d8a7afe2SAdam Hornacek      * </ol>
840d8a7afe2SAdam Hornacek      *
841d8a7afe2SAdam Hornacek      * @return a possible empty set of project names but never {@code null}.
842912cbfdfSAdam Hornacek      * @see QueryParameters#ALL_PROJECT_SEARCH
843d8a7afe2SAdam Hornacek      * @see #PROJECT_PARAM_NAME
844d8a7afe2SAdam Hornacek      * @see #GROUP_PARAM_NAME
845d8a7afe2SAdam Hornacek      * @see #OPEN_GROK_PROJECT
846d8a7afe2SAdam Hornacek      */
847d8a7afe2SAdam Hornacek     public SortedSet<String> getRequestedProjects() {
848d8a7afe2SAdam Hornacek         if (requestedProjects == null) {
849912cbfdfSAdam Hornacek             requestedProjects = getRequestedProjects(
850912cbfdfSAdam Hornacek                     QueryParameters.ALL_PROJECT_SEARCH, PROJECT_PARAM_NAME, GROUP_PARAM_NAME, OPEN_GROK_PROJECT);
851d8a7afe2SAdam Hornacek         }
852d8a7afe2SAdam Hornacek         return requestedProjects;
853d8a7afe2SAdam Hornacek     }
854d8a7afe2SAdam Hornacek 
855d8a7afe2SAdam Hornacek     private static final Pattern COMMA_PATTERN = Pattern.compile(",");
856d8a7afe2SAdam Hornacek 
857d8a7afe2SAdam Hornacek     private static void splitByComma(String value, List<String> result) {
858d8a7afe2SAdam Hornacek         if (value == null || value.length() == 0) {
859d8a7afe2SAdam Hornacek             return;
860d8a7afe2SAdam Hornacek         }
861d8a7afe2SAdam Hornacek         String[] p = COMMA_PATTERN.split(value);
862d8a7afe2SAdam Hornacek         for (String p1 : p) {
863d8a7afe2SAdam Hornacek             if (p1.length() != 0) {
864d8a7afe2SAdam Hornacek                 result.add(p1);
865d8a7afe2SAdam Hornacek             }
866d8a7afe2SAdam Hornacek         }
867d8a7afe2SAdam Hornacek     }
868d8a7afe2SAdam Hornacek 
869d8a7afe2SAdam Hornacek     /**
870d8a7afe2SAdam Hornacek      * Get the cookie values for the given name. Splits comma separated values
871d8a7afe2SAdam Hornacek      * automatically into a list of Strings.
872d8a7afe2SAdam Hornacek      *
873d8a7afe2SAdam Hornacek      * @param cookieName name of the cookie.
874d8a7afe2SAdam Hornacek      * @return a possible empty list.
875d8a7afe2SAdam Hornacek      */
876d8a7afe2SAdam Hornacek     public List<String> getCookieVals(String cookieName) {
877d8a7afe2SAdam Hornacek         Cookie[] cookies = req.getCookies();
878d8a7afe2SAdam Hornacek         ArrayList<String> res = new ArrayList<>();
879d8a7afe2SAdam Hornacek         if (cookies != null) {
880d8a7afe2SAdam Hornacek             for (int i = cookies.length - 1; i >= 0; i--) {
881d8a7afe2SAdam Hornacek                 if (cookies[i].getName().equals(cookieName)) {
882c6f0939bSAdam Hornacek                     String value = URLDecoder.decode(cookies[i].getValue(), StandardCharsets.UTF_8);
883d8a7afe2SAdam Hornacek                     splitByComma(value, res);
884d8a7afe2SAdam Hornacek                 }
885d8a7afe2SAdam Hornacek             }
886d8a7afe2SAdam Hornacek         }
887d8a7afe2SAdam Hornacek         return res;
888d8a7afe2SAdam Hornacek     }
889d8a7afe2SAdam Hornacek 
890d8a7afe2SAdam Hornacek     /**
891d8a7afe2SAdam Hornacek      * Get the parameter values for the given name. Splits comma separated
892d8a7afe2SAdam Hornacek      * values automatically into a list of Strings.
893d8a7afe2SAdam Hornacek      *
894d8a7afe2SAdam Hornacek      * @param paramName name of the parameter.
895d8a7afe2SAdam Hornacek      * @return a possible empty list.
896d8a7afe2SAdam Hornacek      */
897379f387bSVladimir Kotal     private List<String> getParameterValues(String paramName) {
898379f387bSVladimir Kotal         String[] parameterValues = req.getParameterValues(paramName);
899d8a7afe2SAdam Hornacek         List<String> res = new ArrayList<>();
900379f387bSVladimir Kotal         if (parameterValues != null) {
901379f387bSVladimir Kotal             for (int i = parameterValues.length - 1; i >= 0; i--) {
902379f387bSVladimir Kotal                 splitByComma(parameterValues[i], res);
903d8a7afe2SAdam Hornacek             }
904d8a7afe2SAdam Hornacek         }
905d8a7afe2SAdam Hornacek         return res;
906d8a7afe2SAdam Hornacek     }
907d8a7afe2SAdam Hornacek 
908d8a7afe2SAdam Hornacek     /**
909d8a7afe2SAdam Hornacek      * Same as {@link #getRequestedProjects()}, but with a variable cookieName
910d8a7afe2SAdam Hornacek      * and parameter name.
911d8a7afe2SAdam Hornacek      *
912d8a7afe2SAdam Hornacek      * @param searchAllParamName the name of the request parameter corresponding to search all projects.
913d8a7afe2SAdam Hornacek      * @param projectParamName   the name of the request parameter corresponding to a project name.
914d8a7afe2SAdam Hornacek      * @param groupParamName     the name of the request parameter corresponding to a group name
915d8a7afe2SAdam Hornacek      * @param cookieName         name of the cookie which possible contains project
916d8a7afe2SAdam Hornacek      *                           names used as fallback
917d8a7afe2SAdam Hornacek      * @return set of project names. Possibly empty set but never {@code null}.
918d8a7afe2SAdam Hornacek      */
919d8a7afe2SAdam Hornacek     protected SortedSet<String> getRequestedProjects(
920d8a7afe2SAdam Hornacek             String searchAllParamName,
921d8a7afe2SAdam Hornacek             String projectParamName,
922d8a7afe2SAdam Hornacek             String groupParamName,
923d8a7afe2SAdam Hornacek             String cookieName
924d8a7afe2SAdam Hornacek     ) {
925d8a7afe2SAdam Hornacek 
926d8a7afe2SAdam Hornacek         TreeSet<String> projectNames = new TreeSet<>();
927d8a7afe2SAdam Hornacek         List<Project> projects = getEnv().getProjectList();
928d8a7afe2SAdam Hornacek 
929d8a7afe2SAdam Hornacek         if (Boolean.parseBoolean(req.getParameter(searchAllParamName))) {
930d8a7afe2SAdam Hornacek             return getProjectHelper()
931d8a7afe2SAdam Hornacek                     .getAllProjects()
932d8a7afe2SAdam Hornacek                     .stream()
933d8a7afe2SAdam Hornacek                     .map(Project::getName)
934d8a7afe2SAdam Hornacek                     .collect(Collectors.toCollection(TreeSet::new));
935d8a7afe2SAdam Hornacek         }
936d8a7afe2SAdam Hornacek 
937d8a7afe2SAdam Hornacek         // Use a project determined directly from the URL
938d8a7afe2SAdam Hornacek         if (getProject() != null && getProject().isIndexed()) {
939d8a7afe2SAdam Hornacek             projectNames.add(getProject().getName());
940d8a7afe2SAdam Hornacek             return projectNames;
941d8a7afe2SAdam Hornacek         }
942d8a7afe2SAdam Hornacek 
943d8a7afe2SAdam Hornacek         // Use a project if there is just a single project.
944d8a7afe2SAdam Hornacek         if (projects.size() == 1) {
945d8a7afe2SAdam Hornacek             Project p = projects.get(0);
946d8a7afe2SAdam Hornacek             if (p.isIndexed() && authFramework.isAllowed(req, p)) {
947d8a7afe2SAdam Hornacek                 projectNames.add(p.getName());
948d8a7afe2SAdam Hornacek             }
949d8a7afe2SAdam Hornacek             return projectNames;
950d8a7afe2SAdam Hornacek         }
951d8a7afe2SAdam Hornacek 
952d8a7afe2SAdam Hornacek         // Add all projects which match the project parameter name values/
953379f387bSVladimir Kotal         List<String> names = getParameterValues(projectParamName);
954d8a7afe2SAdam Hornacek         for (String projectName : names) {
955d8a7afe2SAdam Hornacek             Project project = Project.getByName(projectName);
956d8a7afe2SAdam Hornacek             if (project != null && project.isIndexed() && authFramework.isAllowed(req, project)) {
957d8a7afe2SAdam Hornacek                 projectNames.add(projectName);
958d8a7afe2SAdam Hornacek             }
959d8a7afe2SAdam Hornacek         }
960d8a7afe2SAdam Hornacek 
961d8a7afe2SAdam Hornacek         // Add all projects which are part of a group that matches the group parameter name.
962379f387bSVladimir Kotal         names = getParameterValues(groupParamName);
963d8a7afe2SAdam Hornacek         for (String groupName : names) {
964d8a7afe2SAdam Hornacek             Group group = Group.getByName(groupName);
965d8a7afe2SAdam Hornacek             if (group != null) {
966d8a7afe2SAdam Hornacek                 projectNames.addAll(getProjectHelper().getAllGrouped(group)
967d8a7afe2SAdam Hornacek                                                       .stream()
968c6f0939bSAdam Hornacek                                                       .filter(Project::isIndexed)
969d8a7afe2SAdam Hornacek                                                       .map(Project::getName)
970d8a7afe2SAdam Hornacek                                                       .collect(Collectors.toSet()));
971d8a7afe2SAdam Hornacek             }
972d8a7afe2SAdam Hornacek         }
973d8a7afe2SAdam Hornacek 
974d8a7afe2SAdam Hornacek         // Add projects based on cookie.
975d8a7afe2SAdam Hornacek         if (projectNames.isEmpty() && getIntParam(QueryParameters.NUM_SELECTED_PARAM, -1) != 0) {
976d8a7afe2SAdam Hornacek             List<String> cookies = getCookieVals(cookieName);
977d8a7afe2SAdam Hornacek             for (String s : cookies) {
978d8a7afe2SAdam Hornacek                 Project x = Project.getByName(s);
979d8a7afe2SAdam Hornacek                 if (x != null && x.isIndexed() && authFramework.isAllowed(req, x)) {
980d8a7afe2SAdam Hornacek                     projectNames.add(s);
981d8a7afe2SAdam Hornacek                 }
982d8a7afe2SAdam Hornacek             }
983d8a7afe2SAdam Hornacek         }
984d8a7afe2SAdam Hornacek 
985d8a7afe2SAdam Hornacek         // Add default projects.
986d8a7afe2SAdam Hornacek         if (projectNames.isEmpty()) {
987d8a7afe2SAdam Hornacek             Set<Project> defaultProjects = env.getDefaultProjects();
988d8a7afe2SAdam Hornacek             if (defaultProjects != null) {
989d8a7afe2SAdam Hornacek                 for (Project project : defaultProjects) {
990d8a7afe2SAdam Hornacek                     if (project.isIndexed() && authFramework.isAllowed(req, project)) {
991d8a7afe2SAdam Hornacek                         projectNames.add(project.getName());
992d8a7afe2SAdam Hornacek                     }
993d8a7afe2SAdam Hornacek                 }
994d8a7afe2SAdam Hornacek             }
995d8a7afe2SAdam Hornacek         }
996d8a7afe2SAdam Hornacek 
997d8a7afe2SAdam Hornacek         return projectNames;
998d8a7afe2SAdam Hornacek     }
999d8a7afe2SAdam Hornacek 
1000d8a7afe2SAdam Hornacek     public ProjectHelper getProjectHelper() {
1001d8a7afe2SAdam Hornacek         return ProjectHelper.getInstance(this);
1002d8a7afe2SAdam Hornacek     }
1003d8a7afe2SAdam Hornacek 
1004d8a7afe2SAdam Hornacek     /**
1005d8a7afe2SAdam Hornacek      * Set the page title to use.
1006d8a7afe2SAdam Hornacek      *
1007d8a7afe2SAdam Hornacek      * @param title title to set (might be {@code null}).
1008d8a7afe2SAdam Hornacek      */
1009d8a7afe2SAdam Hornacek     public void setTitle(String title) {
1010d8a7afe2SAdam Hornacek         pageTitle = title;
1011d8a7afe2SAdam Hornacek     }
1012d8a7afe2SAdam Hornacek 
1013d8a7afe2SAdam Hornacek     /**
1014d8a7afe2SAdam Hornacek      * Get the page title to use.
1015d8a7afe2SAdam Hornacek      *
1016d8a7afe2SAdam Hornacek      * @return {@code null} if not set, the page title otherwise.
1017d8a7afe2SAdam Hornacek      */
1018d8a7afe2SAdam Hornacek     public String getTitle() {
1019d8a7afe2SAdam Hornacek         return pageTitle;
1020d8a7afe2SAdam Hornacek     }
1021d8a7afe2SAdam Hornacek 
1022d8a7afe2SAdam Hornacek     /**
1023d8a7afe2SAdam Hornacek      * Get the base path to use to refer to CSS stylesheets and related
1024d8a7afe2SAdam Hornacek      * resources. Usually used to create links.
1025d8a7afe2SAdam Hornacek      *
1026d8a7afe2SAdam Hornacek      * @return the appropriate application directory prefixed with the
1027d8a7afe2SAdam Hornacek      * application's context path (e.g. "/source/default").
1028d8a7afe2SAdam Hornacek      * @see HttpServletRequest#getContextPath()
1029d8a7afe2SAdam Hornacek      * @see RuntimeEnvironment#getWebappLAF()
1030d8a7afe2SAdam Hornacek      */
1031d8a7afe2SAdam Hornacek     public String getCssDir() {
1032d8a7afe2SAdam Hornacek         return req.getContextPath() + PATH_SEPARATOR + getEnv().getWebappLAF();
1033d8a7afe2SAdam Hornacek     }
1034d8a7afe2SAdam Hornacek 
1035d8a7afe2SAdam Hornacek     /**
1036d8a7afe2SAdam Hornacek      * Get the current runtime environment.
1037d8a7afe2SAdam Hornacek      *
1038d8a7afe2SAdam Hornacek      * @return the runtime env.
1039d8a7afe2SAdam Hornacek      * @see RuntimeEnvironment#getInstance()
1040d8a7afe2SAdam Hornacek      */
1041d8a7afe2SAdam Hornacek     public RuntimeEnvironment getEnv() {
1042d8a7afe2SAdam Hornacek         if (env == null) {
1043d8a7afe2SAdam Hornacek             env = RuntimeEnvironment.getInstance();
1044d8a7afe2SAdam Hornacek         }
1045d8a7afe2SAdam Hornacek         return env;
1046d8a7afe2SAdam Hornacek     }
1047d8a7afe2SAdam Hornacek 
1048d8a7afe2SAdam Hornacek     /**
1049d8a7afe2SAdam Hornacek      * Get the name patterns used to determine, whether a file should be
1050d8a7afe2SAdam Hornacek      * ignored.
1051d8a7afe2SAdam Hornacek      *
1052d8a7afe2SAdam Hornacek      * @return the corresponding value from the current runtime config..
1053d8a7afe2SAdam Hornacek      */
1054d8a7afe2SAdam Hornacek     public IgnoredNames getIgnoredNames() {
1055d8a7afe2SAdam Hornacek         if (ignoredNames == null) {
1056d8a7afe2SAdam Hornacek             ignoredNames = getEnv().getIgnoredNames();
1057d8a7afe2SAdam Hornacek         }
1058d8a7afe2SAdam Hornacek         return ignoredNames;
1059d8a7afe2SAdam Hornacek     }
1060d8a7afe2SAdam Hornacek 
1061d8a7afe2SAdam Hornacek     /**
1062d8a7afe2SAdam Hornacek      * Get the canonical path to root of the source tree. File separators are
1063d8a7afe2SAdam Hornacek      * replaced with a {@link org.opengrok.indexer.index.Indexer#PATH_SEPARATOR}.
1064d8a7afe2SAdam Hornacek      *
1065d8a7afe2SAdam Hornacek      * @return The on disk source root directory.
1066d8a7afe2SAdam Hornacek      * @see RuntimeEnvironment#getSourceRootPath()
1067d8a7afe2SAdam Hornacek      */
1068d8a7afe2SAdam Hornacek     public String getSourceRootPath() {
1069d8a7afe2SAdam Hornacek         if (sourceRootPath == null) {
1070d8a7afe2SAdam Hornacek             String srcpath = getEnv().getSourceRootPath();
1071d8a7afe2SAdam Hornacek             if (srcpath != null) {
1072d8a7afe2SAdam Hornacek                 sourceRootPath = srcpath.replace(File.separatorChar, PATH_SEPARATOR);
1073d8a7afe2SAdam Hornacek             }
1074d8a7afe2SAdam Hornacek         }
1075d8a7afe2SAdam Hornacek         return sourceRootPath;
1076d8a7afe2SAdam Hornacek     }
1077d8a7afe2SAdam Hornacek 
1078d8a7afe2SAdam Hornacek     /**
1079d8a7afe2SAdam Hornacek      * Get the prefix for the related request.
1080d8a7afe2SAdam Hornacek      *
1081d8a7afe2SAdam Hornacek      * @return {@link Prefix#UNKNOWN} if the servlet path matches any known
1082d8a7afe2SAdam Hornacek      * prefix, the prefix otherwise.
1083d8a7afe2SAdam Hornacek      */
1084d8a7afe2SAdam Hornacek     public Prefix getPrefix() {
1085d8a7afe2SAdam Hornacek         if (prefix == null) {
1086d8a7afe2SAdam Hornacek             prefix = Prefix.get(req.getServletPath());
1087d8a7afe2SAdam Hornacek         }
1088d8a7afe2SAdam Hornacek         return prefix;
1089d8a7afe2SAdam Hornacek     }
1090d8a7afe2SAdam Hornacek 
1091d8a7afe2SAdam Hornacek     /**
1092d8a7afe2SAdam Hornacek      * Get the canonical path of the related resource relative to the source
1093379f387bSVladimir Kotal      * root directory (used file separators are all {@link org.opengrok.indexer.index.Indexer#PATH_SEPARATOR}).
1094379f387bSVladimir Kotal      * No check is made, whether the obtained path is really an accessible resource on disk.
1095d8a7afe2SAdam Hornacek      *
1096d8a7afe2SAdam Hornacek      * @see HttpServletRequest#getPathInfo()
1097d8a7afe2SAdam Hornacek      * @return a possible empty String (denotes the source root directory) but
1098d8a7afe2SAdam Hornacek      * not {@code null}.
1099d8a7afe2SAdam Hornacek      */
1100d8a7afe2SAdam Hornacek     public String getPath() {
1101d8a7afe2SAdam Hornacek         if (path == null) {
1102c6d5bfc3SVladimir Kotal             path = Util.getCanonicalPath(Laundromat.launderInput(req.getPathInfo()), PATH_SEPARATOR);
1103d8a7afe2SAdam Hornacek             if (PATH_SEPARATOR_STRING.equals(path)) {
1104d8a7afe2SAdam Hornacek                 path = "";
1105d8a7afe2SAdam Hornacek             }
1106d8a7afe2SAdam Hornacek         }
1107d8a7afe2SAdam Hornacek         return path;
1108d8a7afe2SAdam Hornacek     }
1109d8a7afe2SAdam Hornacek 
111057acd71dSVladimir Kotal     /**
1111379f387bSVladimir Kotal      * @return true if file/directory corresponding to the request path exists however is unreadable, false otherwise
111257acd71dSVladimir Kotal      */
11139554a08aSVladimir Kotal     public boolean isUnreadable() {
11149554a08aSVladimir Kotal         File f = new File(getSourceRootPath(), getPath());
11159554a08aSVladimir Kotal         return f.exists() && !f.canRead();
11169554a08aSVladimir Kotal     }
11179554a08aSVladimir Kotal 
1118d8a7afe2SAdam Hornacek     /**
1119d8a7afe2SAdam Hornacek      * Get the on disk file for the given path.
1120d8a7afe2SAdam Hornacek      *
1121d8a7afe2SAdam Hornacek      * NOTE: If a repository contains hard or symbolic links, the returned file
1122379f387bSVladimir Kotal      * may finally point to a file outside the source root directory.
1123d8a7afe2SAdam Hornacek      *
1124d8a7afe2SAdam Hornacek      * @param path the path to the file relatively to the source root
1125d8a7afe2SAdam Hornacek      * @return null if the related file or directory is not
1126d8a7afe2SAdam Hornacek      * available (can not be find below the source root directory), the readable
1127d8a7afe2SAdam Hornacek      * file or directory otherwise.
1128d8a7afe2SAdam Hornacek      * @see #getSourceRootPath()
1129d8a7afe2SAdam Hornacek      */
1130d8a7afe2SAdam Hornacek     public File getResourceFile(String path) {
1131d8a7afe2SAdam Hornacek         File f;
1132d8a7afe2SAdam Hornacek         f = new File(getSourceRootPath(), path);
1133d8a7afe2SAdam Hornacek         if (!f.canRead()) {
1134d8a7afe2SAdam Hornacek             return null;
1135d8a7afe2SAdam Hornacek         }
1136d8a7afe2SAdam Hornacek         return f;
1137d8a7afe2SAdam Hornacek     }
1138d8a7afe2SAdam Hornacek 
1139d8a7afe2SAdam Hornacek     /**
1140d8a7afe2SAdam Hornacek      * Get the on disk file to the request related file or directory.
1141d8a7afe2SAdam Hornacek      *
1142d8a7afe2SAdam Hornacek      * NOTE: If a repository contains hard or symbolic links, the returned file
1143379f387bSVladimir Kotal      * may finally point to a file outside the source root directory.
1144d8a7afe2SAdam Hornacek      *
1145379f387bSVladimir Kotal      * @return {@code new File({@link org.opengrok.indexer.index.Indexer#PATH_SEPARATOR_STRING })}
1146379f387bSVladimir Kotal      * if the related file or directory is not available (can not be find below the source root directory),
1147379f387bSVladimir Kotal      * the readable file or directory otherwise.
1148d8a7afe2SAdam Hornacek      * @see #getSourceRootPath()
1149d8a7afe2SAdam Hornacek      * @see #getPath()
1150d8a7afe2SAdam Hornacek      */
1151d8a7afe2SAdam Hornacek     public File getResourceFile() {
1152d8a7afe2SAdam Hornacek         if (resourceFile == null) {
1153d8a7afe2SAdam Hornacek             resourceFile = getResourceFile(getPath());
1154d8a7afe2SAdam Hornacek             if (resourceFile == null) {
1155d8a7afe2SAdam Hornacek                 resourceFile = new File(PATH_SEPARATOR_STRING);
1156d8a7afe2SAdam Hornacek             }
1157d8a7afe2SAdam Hornacek         }
1158d8a7afe2SAdam Hornacek         return resourceFile;
1159d8a7afe2SAdam Hornacek     }
1160d8a7afe2SAdam Hornacek 
1161d8a7afe2SAdam Hornacek     /**
1162d8a7afe2SAdam Hornacek      * Get the canonical on disk path to the request related file or directory
1163d8a7afe2SAdam Hornacek      * with all file separators replaced by a {@link org.opengrok.indexer.index.Indexer#PATH_SEPARATOR}.
1164d8a7afe2SAdam Hornacek      *
1165379f387bSVladimir Kotal      * @return {@link org.opengrok.indexer.index.Indexer#PATH_SEPARATOR_STRING} if the evaluated path is invalid
1166379f387bSVladimir Kotal      * or outside the source root directory, otherwise the path to the readable file or directory.
1167d8a7afe2SAdam Hornacek      * @see #getResourceFile()
1168d8a7afe2SAdam Hornacek      */
1169d8a7afe2SAdam Hornacek     public String getResourcePath() {
1170d8a7afe2SAdam Hornacek         if (resourcePath == null) {
1171d8a7afe2SAdam Hornacek             resourcePath = Util.fixPathIfWindows(getResourceFile().getPath());
1172d8a7afe2SAdam Hornacek         }
1173d8a7afe2SAdam Hornacek         return resourcePath;
1174d8a7afe2SAdam Hornacek     }
1175d8a7afe2SAdam Hornacek 
1176d8a7afe2SAdam Hornacek     /**
1177d8a7afe2SAdam Hornacek      * Check, whether the related request resource matches a valid file or
1178d8a7afe2SAdam Hornacek      * directory below the source root directory and whether it matches an
1179d8a7afe2SAdam Hornacek      * ignored pattern.
1180d8a7afe2SAdam Hornacek      *
1181379f387bSVladimir Kotal      * @return {@code true} if the related resource does not exist or should be ignored.
1182d8a7afe2SAdam Hornacek      * @see #getIgnoredNames()
1183d8a7afe2SAdam Hornacek      * @see #getResourcePath()
1184d8a7afe2SAdam Hornacek      */
1185d8a7afe2SAdam Hornacek     public boolean resourceNotAvailable() {
1186d8a7afe2SAdam Hornacek         getIgnoredNames();
1187d8a7afe2SAdam Hornacek         return getResourcePath().equals(PATH_SEPARATOR_STRING) || ignoredNames.ignore(getPath())
1188d8a7afe2SAdam Hornacek                 || ignoredNames.ignore(resourceFile.getParentFile())
1189d8a7afe2SAdam Hornacek                 || ignoredNames.ignore(resourceFile);
1190d8a7afe2SAdam Hornacek     }
1191d8a7afe2SAdam Hornacek 
1192d8a7afe2SAdam Hornacek     /**
1193d8a7afe2SAdam Hornacek      * Check, whether the request related path represents a directory.
1194d8a7afe2SAdam Hornacek      *
1195d8a7afe2SAdam Hornacek      * @return {@code true} if directory related request
1196d8a7afe2SAdam Hornacek      */
1197d8a7afe2SAdam Hornacek     public boolean isDir() {
1198d8a7afe2SAdam Hornacek         if (isDir == null) {
1199d8a7afe2SAdam Hornacek             isDir = getResourceFile().isDirectory();
1200d8a7afe2SAdam Hornacek         }
1201d8a7afe2SAdam Hornacek         return isDir;
1202d8a7afe2SAdam Hornacek     }
1203d8a7afe2SAdam Hornacek 
1204d8a7afe2SAdam Hornacek     private static String trailingSlash(String path) {
1205d8a7afe2SAdam Hornacek         return path.length() == 0 || path.charAt(path.length() - 1) != PATH_SEPARATOR
1206d8a7afe2SAdam Hornacek                 ? PATH_SEPARATOR_STRING
1207d8a7afe2SAdam Hornacek                 : "";
1208d8a7afe2SAdam Hornacek     }
1209d8a7afe2SAdam Hornacek 
12107b79f34fSVladimir Kotal     private File checkFileInner(File file, File dir, String name) {
12117b79f34fSVladimir Kotal         File f = new File(dir, name);
12127b79f34fSVladimir Kotal         if (f.exists() && f.isFile()) {
12137b79f34fSVladimir Kotal             if (f.lastModified() >= file.lastModified()) {
12147b79f34fSVladimir Kotal                 return f;
12157b79f34fSVladimir Kotal             } else {
12167b79f34fSVladimir Kotal                 LOGGER.log(Level.WARNING, "file ''{0}'' is newer than ''{1}''", new Object[]{file, f});
12177b79f34fSVladimir Kotal             }
12187b79f34fSVladimir Kotal         }
12197b79f34fSVladimir Kotal 
12207b79f34fSVladimir Kotal         return null;
12217b79f34fSVladimir Kotal     }
12227b79f34fSVladimir Kotal 
122392f4530fSVladimir Kotal     private File checkFile(File file, File dir, String name, boolean compressed) {
1224d8a7afe2SAdam Hornacek         File f;
1225d8a7afe2SAdam Hornacek         if (compressed) {
12267b79f34fSVladimir Kotal             f = checkFileInner(file, dir, TandemPath.join(name, ".gz"));
12277b79f34fSVladimir Kotal             if (f != null) {
1228d8a7afe2SAdam Hornacek                 return f;
1229d8a7afe2SAdam Hornacek             }
1230d8a7afe2SAdam Hornacek         }
12317b79f34fSVladimir Kotal 
12327b79f34fSVladimir Kotal         return checkFileInner(file, dir, name);
1233d8a7afe2SAdam Hornacek     }
1234d8a7afe2SAdam Hornacek 
1235d8a7afe2SAdam Hornacek     private File checkFileResolve(File dir, String name, boolean compressed) {
1236d8a7afe2SAdam Hornacek         File lresourceFile = new File(getSourceRootPath() + getPath(), name);
1237d8a7afe2SAdam Hornacek         if (!lresourceFile.canRead()) {
1238d8a7afe2SAdam Hornacek             lresourceFile = new File(PATH_SEPARATOR_STRING);
1239d8a7afe2SAdam Hornacek         }
124092f4530fSVladimir Kotal 
124192f4530fSVladimir Kotal         return checkFile(lresourceFile, dir, name, compressed);
1242d8a7afe2SAdam Hornacek     }
1243d8a7afe2SAdam Hornacek 
1244d8a7afe2SAdam Hornacek     /**
1245d8a7afe2SAdam Hornacek      * Find the files with the given names in the {@link #getPath()} directory
1246379f387bSVladimir Kotal      * relative to the cross-file directory of the opengrok data directory. It is
1247d8a7afe2SAdam Hornacek      * tried to find the compressed file first by appending the file extension
1248d8a7afe2SAdam Hornacek      * ".gz" to the filename. If that fails or an uncompressed version of the
1249d8a7afe2SAdam Hornacek      * file is younger than its compressed version, the uncompressed file gets
1250d8a7afe2SAdam Hornacek      * used.
1251d8a7afe2SAdam Hornacek      *
1252d8a7afe2SAdam Hornacek      * @param filenames filenames to lookup.
1253d8a7afe2SAdam Hornacek      * @return an empty array if the related directory does not exist or the
1254d8a7afe2SAdam Hornacek      * given list is {@code null} or empty, otherwise an array, which may
1255d8a7afe2SAdam Hornacek      * contain {@code null} entries (when the related file could not be found)
1256d8a7afe2SAdam Hornacek      * having the same order as the given list.
1257d8a7afe2SAdam Hornacek      */
1258d8a7afe2SAdam Hornacek     public File[] findDataFiles(List<String> filenames) {
1259d8a7afe2SAdam Hornacek         if (filenames == null || filenames.isEmpty()) {
1260d8a7afe2SAdam Hornacek             return new File[0];
1261d8a7afe2SAdam Hornacek         }
1262d8a7afe2SAdam Hornacek         File[] res = new File[filenames.size()];
1263d8a7afe2SAdam Hornacek         File dir = new File(getEnv().getDataRootPath() + Prefix.XREF_P + getPath());
1264d8a7afe2SAdam Hornacek         if (dir.exists() && dir.isDirectory()) {
1265d8a7afe2SAdam Hornacek             getResourceFile();
1266d8a7afe2SAdam Hornacek             boolean compressed = getEnv().isCompressXref();
1267d8a7afe2SAdam Hornacek             for (int i = res.length - 1; i >= 0; i--) {
1268d8a7afe2SAdam Hornacek                 res[i] = checkFileResolve(dir, filenames.get(i), compressed);
1269d8a7afe2SAdam Hornacek             }
1270d8a7afe2SAdam Hornacek         }
1271d8a7afe2SAdam Hornacek         return res;
1272d8a7afe2SAdam Hornacek     }
1273d8a7afe2SAdam Hornacek 
1274d8a7afe2SAdam Hornacek     /**
1275379f387bSVladimir Kotal      * Lookup the file {@link #getPath()} relative to the cross-file directory of
1276d8a7afe2SAdam Hornacek      * the opengrok data directory. It is tried to find the compressed file
1277d8a7afe2SAdam Hornacek      * first by appending the file extension ".gz" to the filename. If that
1278d8a7afe2SAdam Hornacek      * fails or an uncompressed version of the file is younger than its
1279d8a7afe2SAdam Hornacek      * compressed version, the uncompressed file gets used.
1280d8a7afe2SAdam Hornacek      *
1281d8a7afe2SAdam Hornacek      * @return {@code null} if not found, the file otherwise.
1282d8a7afe2SAdam Hornacek      */
1283d8a7afe2SAdam Hornacek     public File findDataFile() {
128492f4530fSVladimir Kotal         return checkFile(resourceFile, new File(getEnv().getDataRootPath() + Prefix.XREF_P),
1285d8a7afe2SAdam Hornacek                 getPath(), env.isCompressXref());
1286d8a7afe2SAdam Hornacek     }
1287d8a7afe2SAdam Hornacek 
128836b6a6f8SVladimir Kotal     /**
128936b6a6f8SVladimir Kotal      * @return last revision string for {@code file} or null
129036b6a6f8SVladimir Kotal      */
1291a964b78cSVladimir Kotal     @Nullable
1292d8a7afe2SAdam Hornacek     public String getLatestRevision() {
1293d8a7afe2SAdam Hornacek         if (!getEnv().isHistoryEnabled()) {
1294094a9b1eSVladimir Kotal             LOGGER.log(Level.FINE, "will not get latest revision for ''{0}'' as history is disabled",
1295094a9b1eSVladimir Kotal                     getResourceFile());
1296d8a7afe2SAdam Hornacek             return null;
1297d8a7afe2SAdam Hornacek         }
1298d8a7afe2SAdam Hornacek 
1299a964b78cSVladimir Kotal         // Try getting the history revision from the index first.
130036b6a6f8SVladimir Kotal         String lastRev = getLastRevFromIndex();
130136b6a6f8SVladimir Kotal         if (lastRev != null) {
1302094a9b1eSVladimir Kotal             LOGGER.log(Level.FINEST, "got last revision of ''{0}'' from the index", getResourceFile());
130336b6a6f8SVladimir Kotal             return lastRev;
130436b6a6f8SVladimir Kotal         }
130536b6a6f8SVladimir Kotal 
1306a964b78cSVladimir Kotal         // If this is older index, fallback to the history (either fetch from history cache or retrieve from
1307a964b78cSVladimir Kotal         // the repository directly).
1308a903f806SVladimir Kotal         try {
130936b6a6f8SVladimir Kotal             return getLastRevFromHistory();
1310a903f806SVladimir Kotal         } catch (HistoryException e) {
1311094a9b1eSVladimir Kotal             LOGGER.log(Level.WARNING, "cannot get latest revision for ''{0}'' using history", getPath());
1312a903f806SVladimir Kotal             return null;
1313a903f806SVladimir Kotal         }
131436b6a6f8SVladimir Kotal     }
131536b6a6f8SVladimir Kotal 
131636b6a6f8SVladimir Kotal     @Nullable
1317a903f806SVladimir Kotal     private String getLastRevFromHistory() throws HistoryException {
1318a964b78cSVladimir Kotal         File file = new File(getEnv().getSourceRootFile(), getPath());
1319a964b78cSVladimir Kotal         HistoryEntry he = HistoryGuru.getInstance().getLastHistoryEntry(file, true);
1320a903f806SVladimir Kotal         if (he != null) {
1321d8a7afe2SAdam Hornacek             return he.getRevision();
1322d8a7afe2SAdam Hornacek         }
1323d8a7afe2SAdam Hornacek 
1324a903f806SVladimir Kotal         return null;
1325a903f806SVladimir Kotal     }
1326a903f806SVladimir Kotal 
13278ff8eecbSVladimir Kotal     /**
13288ff8eecbSVladimir Kotal      * Retrieve last revision from the document matching the resource file (if any).
1329df712980SVladimir Kotal      * @return last revision or {@code null} if the document cannot be found, is out of sync
1330df712980SVladimir Kotal      * w.r.t. last modified time of the file or the last commit ID is not stored in the document.
13318ff8eecbSVladimir Kotal      */
133236b6a6f8SVladimir Kotal     @Nullable
1333*c4b39070SVladimir Kotal     @VisibleForTesting
1334*c4b39070SVladimir Kotal     String getLastRevFromIndex() {
133536b6a6f8SVladimir Kotal         Document doc = null;
133636b6a6f8SVladimir Kotal         try {
133736b6a6f8SVladimir Kotal             doc = IndexDatabase.getDocument(getResourceFile());
1338df712980SVladimir Kotal         } catch (Exception e) {
1339df712980SVladimir Kotal             LOGGER.log(Level.WARNING, String.format("cannot get document for %s", path), e);
1340df712980SVladimir Kotal         }
1341df712980SVladimir Kotal 
1342df712980SVladimir Kotal         String lastRev = null;
13438ff8eecbSVladimir Kotal         if (doc != null) {
1344df712980SVladimir Kotal             // There is no point of checking the date if the LASTREV field is not present.
1345df712980SVladimir Kotal             lastRev = doc.get(QueryBuilder.LASTREV);
1346df712980SVladimir Kotal             if (lastRev != null) {
1347df712980SVladimir Kotal                 Date docDate;
1348df712980SVladimir Kotal                 try {
1349df712980SVladimir Kotal                     docDate = DateTools.stringToDate(doc.get(QueryBuilder.DATE));
1350df712980SVladimir Kotal                 } catch (ParseException e) {
1351df712980SVladimir Kotal                     LOGGER.log(Level.WARNING, String.format("cannot get date from the document %s", doc), e);
1352df712980SVladimir Kotal                     return null;
1353df712980SVladimir Kotal                 }
13548ff8eecbSVladimir Kotal                 Date fileDate = new Date(getResourceFile().lastModified());
13558ff8eecbSVladimir Kotal                 if (docDate.compareTo(fileDate) < 0) {
1356379f387bSVladimir Kotal                     LOGGER.log(Level.FINER, "document for ''{0}'' is out of sync", getResourceFile());
13578ff8eecbSVladimir Kotal                     return null;
13588ff8eecbSVladimir Kotal                 }
13598ff8eecbSVladimir Kotal             }
136036b6a6f8SVladimir Kotal         }
136136b6a6f8SVladimir Kotal 
1362df712980SVladimir Kotal         return lastRev;
136336b6a6f8SVladimir Kotal     }
136436b6a6f8SVladimir Kotal 
1365d8a7afe2SAdam Hornacek     /**
1366d8a7afe2SAdam Hornacek      * Is revision the latest revision ?
1367d8a7afe2SAdam Hornacek      * @param rev revision string
1368d8a7afe2SAdam Hornacek      * @return true if latest revision, false otherwise
1369d8a7afe2SAdam Hornacek      */
1370d8a7afe2SAdam Hornacek     public boolean isLatestRevision(String rev) {
1371d8a7afe2SAdam Hornacek         return rev.equals(getLatestRevision());
1372d8a7afe2SAdam Hornacek     }
1373d8a7afe2SAdam Hornacek 
1374d8a7afe2SAdam Hornacek     /**
1375379f387bSVladimir Kotal      * Get the location of cross-reference for given file containing the given revision.
1376d8a7afe2SAdam Hornacek      * @param revStr defined revision string
1377d8a7afe2SAdam Hornacek      * @return location to redirect to
1378d8a7afe2SAdam Hornacek      */
1379d8a7afe2SAdam Hornacek     public String getRevisionLocation(String revStr) {
1380d8a7afe2SAdam Hornacek         StringBuilder sb = new StringBuilder();
1381d8a7afe2SAdam Hornacek 
1382d8a7afe2SAdam Hornacek         sb.append(req.getContextPath());
1383d8a7afe2SAdam Hornacek         sb.append(Prefix.XREF_P);
1384d6df19e1SAdam Hornacek         sb.append(Util.uriEncodePath(getPath()));
1385d8a7afe2SAdam Hornacek         sb.append("?");
1386d8a7afe2SAdam Hornacek         sb.append(QueryParameters.REVISION_PARAM_EQ);
1387d6df19e1SAdam Hornacek         sb.append(Util.uriEncode(revStr));
1388d8a7afe2SAdam Hornacek 
1389d8a7afe2SAdam Hornacek         if (req.getQueryString() != null) {
1390d8a7afe2SAdam Hornacek             sb.append("&");
1391d8a7afe2SAdam Hornacek             sb.append(req.getQueryString());
1392d8a7afe2SAdam Hornacek         }
1393d8a7afe2SAdam Hornacek         if (fragmentIdentifier != null) {
1394d6df19e1SAdam Hornacek             String anchor = Util.uriEncode(fragmentIdentifier);
1395d8a7afe2SAdam Hornacek 
1396d8a7afe2SAdam Hornacek             String reqFrag = req.getParameter(QueryParameters.FRAGMENT_IDENTIFIER_PARAM);
1397d8a7afe2SAdam Hornacek             if (reqFrag == null || reqFrag.isEmpty()) {
1398d8a7afe2SAdam Hornacek                 /*
1399d8a7afe2SAdam Hornacek                  * We've determined that the fragmentIdentifier field must have
1400d8a7afe2SAdam Hornacek                  * been set to augment request parameters. Now include it
1401d8a7afe2SAdam Hornacek                  * explicitly in the next request parameters.
1402d8a7afe2SAdam Hornacek                  */
1403d8a7afe2SAdam Hornacek                 sb.append("&");
1404d8a7afe2SAdam Hornacek                 sb.append(QueryParameters.FRAGMENT_IDENTIFIER_PARAM_EQ);
1405d8a7afe2SAdam Hornacek                 sb.append(anchor);
1406d8a7afe2SAdam Hornacek             }
1407d8a7afe2SAdam Hornacek             sb.append("#");
1408d8a7afe2SAdam Hornacek             sb.append(anchor);
1409d8a7afe2SAdam Hornacek         }
1410d8a7afe2SAdam Hornacek 
1411d8a7afe2SAdam Hornacek         return sb.toString();
1412d8a7afe2SAdam Hornacek     }
1413d8a7afe2SAdam Hornacek 
1414d8a7afe2SAdam Hornacek     /**
1415d8a7afe2SAdam Hornacek      * Get the path the request should be redirected (if any).
1416d8a7afe2SAdam Hornacek      *
1417d8a7afe2SAdam Hornacek      * @return {@code null} if there is no reason to redirect, the URI encoded
1418d8a7afe2SAdam Hornacek      * redirect path to use otherwise.
1419d8a7afe2SAdam Hornacek      */
1420d8a7afe2SAdam Hornacek     public String getDirectoryRedirect() {
1421d8a7afe2SAdam Hornacek         if (isDir()) {
1422d8a7afe2SAdam Hornacek             getPrefix();
1423379f387bSVladimir Kotal             // Redirect /xref -> /xref/
1424d8a7afe2SAdam Hornacek             if (prefix == Prefix.XREF_P
1425d8a7afe2SAdam Hornacek                     && getUriEncodedPath().isEmpty()
1426d8a7afe2SAdam Hornacek                     && !req.getRequestURI().endsWith("/")) {
1427d8a7afe2SAdam Hornacek                 return req.getContextPath() + Prefix.XREF_P + '/';
1428d8a7afe2SAdam Hornacek             }
1429d8a7afe2SAdam Hornacek 
1430d8a7afe2SAdam Hornacek             if (getPath().length() == 0) {
1431d8a7afe2SAdam Hornacek                 // => /
1432d8a7afe2SAdam Hornacek                 return null;
1433d8a7afe2SAdam Hornacek             }
1434d8a7afe2SAdam Hornacek 
1435d8a7afe2SAdam Hornacek             if (prefix != Prefix.XREF_P && prefix != Prefix.HIST_L
1436d8a7afe2SAdam Hornacek                     && prefix != Prefix.RSS_P) {
1437d8a7afe2SAdam Hornacek                 // if it is an existing dir perhaps people wanted dir xref
1438d8a7afe2SAdam Hornacek                 return req.getContextPath() + Prefix.XREF_P
1439d8a7afe2SAdam Hornacek                         + getUriEncodedPath() + trailingSlash(getPath());
1440d8a7afe2SAdam Hornacek             }
1441d8a7afe2SAdam Hornacek             String ts = trailingSlash(getPath());
1442d8a7afe2SAdam Hornacek             if (ts.length() != 0) {
1443d8a7afe2SAdam Hornacek                 return req.getContextPath() + prefix + getUriEncodedPath() + ts;
1444d8a7afe2SAdam Hornacek             }
1445d8a7afe2SAdam Hornacek         }
1446d8a7afe2SAdam Hornacek         return null;
1447d8a7afe2SAdam Hornacek     }
1448d8a7afe2SAdam Hornacek 
1449d8a7afe2SAdam Hornacek     /**
1450d8a7afe2SAdam Hornacek      * Get the URI encoded canonical path to the related file or directory (the
1451d8a7afe2SAdam Hornacek      * URI part between the servlet path and the start of the query string).
1452d8a7afe2SAdam Hornacek      *
1453379f387bSVladimir Kotal      * @return a URI encoded path which might be an empty string but not {@code null}.
1454d8a7afe2SAdam Hornacek      * @see #getPath()
1455d8a7afe2SAdam Hornacek      */
1456d8a7afe2SAdam Hornacek     public String getUriEncodedPath() {
1457d8a7afe2SAdam Hornacek         if (uriEncodedPath == null) {
1458d6df19e1SAdam Hornacek             uriEncodedPath = Util.uriEncodePath(getPath());
1459d8a7afe2SAdam Hornacek         }
1460d8a7afe2SAdam Hornacek         return uriEncodedPath;
1461d8a7afe2SAdam Hornacek     }
1462d8a7afe2SAdam Hornacek 
1463d8a7afe2SAdam Hornacek     /**
1464d8a7afe2SAdam Hornacek      * Add a new file script to the page by the name.
1465d8a7afe2SAdam Hornacek      *
1466d8a7afe2SAdam Hornacek      * @param name name of the script to search for
1467d8a7afe2SAdam Hornacek      * @return this
1468d8a7afe2SAdam Hornacek      *
1469d8a7afe2SAdam Hornacek      * @see Scripts#addScript(String, String, Scripts.Type)
1470d8a7afe2SAdam Hornacek      */
1471d8a7afe2SAdam Hornacek     public PageConfig addScript(String name) {
1472d8a7afe2SAdam Hornacek         this.scripts.addScript(this.req.getContextPath(), name, isDebug() ? Scripts.Type.DEBUG : Scripts.Type.MINIFIED);
1473d8a7afe2SAdam Hornacek         return this;
1474d8a7afe2SAdam Hornacek     }
1475d8a7afe2SAdam Hornacek 
1476d8a7afe2SAdam Hornacek     private boolean isDebug() {
1477d8a7afe2SAdam Hornacek         return Boolean.parseBoolean(req.getParameter(DEBUG_PARAM_NAME));
1478d8a7afe2SAdam Hornacek     }
1479d8a7afe2SAdam Hornacek 
1480d8a7afe2SAdam Hornacek     /**
1481d8a7afe2SAdam Hornacek      * Return the page scripts.
1482d8a7afe2SAdam Hornacek      *
1483d8a7afe2SAdam Hornacek      * @return the scripts
1484d8a7afe2SAdam Hornacek      *
1485d8a7afe2SAdam Hornacek      * @see Scripts
1486d8a7afe2SAdam Hornacek      */
1487d8a7afe2SAdam Hornacek     public Scripts getScripts() {
1488d8a7afe2SAdam Hornacek         return this.scripts;
1489d8a7afe2SAdam Hornacek     }
1490d8a7afe2SAdam Hornacek 
1491d8a7afe2SAdam Hornacek     /**
1492d8a7afe2SAdam Hornacek      * Get opengrok's configured data root directory. It is verified, that the
1493d8a7afe2SAdam Hornacek      * used environment has a valid opengrok data root set and that it is an
1494d8a7afe2SAdam Hornacek      * accessible directory.
1495d8a7afe2SAdam Hornacek      *
1496d8a7afe2SAdam Hornacek      * @return the opengrok data directory.
1497d8a7afe2SAdam Hornacek      * @throws InvalidParameterException if inaccessible or not set.
1498d8a7afe2SAdam Hornacek      */
1499d8a7afe2SAdam Hornacek     public File getDataRoot() {
1500d8a7afe2SAdam Hornacek         if (dataRoot == null) {
1501d8a7afe2SAdam Hornacek             String tmp = getEnv().getDataRootPath();
1502d8a7afe2SAdam Hornacek             if (tmp == null || tmp.length() == 0) {
1503d8a7afe2SAdam Hornacek                 throw new InvalidParameterException("dataRoot parameter is not "
1504d8a7afe2SAdam Hornacek                         + "set in configuration.xml!");
1505d8a7afe2SAdam Hornacek             }
1506d8a7afe2SAdam Hornacek             dataRoot = new File(tmp);
1507d8a7afe2SAdam Hornacek             if (!(dataRoot.isDirectory() && dataRoot.canRead())) {
1508d8a7afe2SAdam Hornacek                 throw new InvalidParameterException("The configured dataRoot '"
1509d8a7afe2SAdam Hornacek                         + tmp
1510d8a7afe2SAdam Hornacek                         + "' refers to a none-existing or unreadable directory!");
1511d8a7afe2SAdam Hornacek             }
1512d8a7afe2SAdam Hornacek         }
1513d8a7afe2SAdam Hornacek         return dataRoot;
1514d8a7afe2SAdam Hornacek     }
1515d8a7afe2SAdam Hornacek 
1516d8a7afe2SAdam Hornacek     /**
15170516dd21SVladimir Kotal      * Play nice in reverse proxy environment by using pre-configured hostname
15180516dd21SVladimir Kotal      * request to construct the URLs.
15190516dd21SVladimir Kotal      * Will not work well if the scheme or port is different for proxied server
15200516dd21SVladimir Kotal      * and original server.
15210516dd21SVladimir Kotal      * @return server name
15220516dd21SVladimir Kotal      */
15230516dd21SVladimir Kotal     public String getServerName() {
15240516dd21SVladimir Kotal         if (env.getServerName() != null) {
15250516dd21SVladimir Kotal             return env.getServerName();
15260516dd21SVladimir Kotal         } else {
15270516dd21SVladimir Kotal             return req.getServerName();
15280516dd21SVladimir Kotal         }
15290516dd21SVladimir Kotal     }
15300516dd21SVladimir Kotal 
15310516dd21SVladimir Kotal     /**
1532d8a7afe2SAdam Hornacek      * Prepare a search helper with all required information, ready to execute
1533d8a7afe2SAdam Hornacek      * the query implied by the related request parameters and cookies.
1534d8a7afe2SAdam Hornacek      * <p>
1535ae1c323bSVladimir Kotal      * NOTE: One should check the {@link SearchHelper#getErrorMsg()} as well as
1536ae1c323bSVladimir Kotal      * {@link SearchHelper#getRedirect()} and take the appropriate action before
1537d8a7afe2SAdam Hornacek      * executing the prepared query or continue processing.
1538d8a7afe2SAdam Hornacek      * <p>
1539d8a7afe2SAdam Hornacek      * This method stops populating fields as soon as an error occurs.
1540d8a7afe2SAdam Hornacek      *
1541d8a7afe2SAdam Hornacek      * @return a search helper.
1542d8a7afe2SAdam Hornacek      */
1543d8a7afe2SAdam Hornacek     public SearchHelper prepareSearch() {
1544d8a7afe2SAdam Hornacek         List<SortOrder> sortOrders = getSortOrder();
1545ae1c323bSVladimir Kotal         SearchHelper sh = prepareInternalSearch(sortOrders.isEmpty() ? SortOrder.RELEVANCY : sortOrders.get(0));
1546d8a7afe2SAdam Hornacek 
1547d8a7afe2SAdam Hornacek         if (getRequestedProjects().isEmpty() && getEnv().hasProjects()) {
1548ae1c323bSVladimir Kotal             sh.setErrorMsg("You must select a project!");
1549d8a7afe2SAdam Hornacek             return sh;
1550d8a7afe2SAdam Hornacek         }
1551d8a7afe2SAdam Hornacek 
1552ae1c323bSVladimir Kotal         if (sh.getBuilder().getSize() == 0) {
1553d8a7afe2SAdam Hornacek             // Entry page show the map
1554ae1c323bSVladimir Kotal             sh.setRedirect(req.getContextPath() + '/');
1555d8a7afe2SAdam Hornacek             return sh;
1556d8a7afe2SAdam Hornacek         }
1557d8a7afe2SAdam Hornacek 
1558d8a7afe2SAdam Hornacek         return sh;
1559d8a7afe2SAdam Hornacek     }
1560d8a7afe2SAdam Hornacek 
1561d8a7afe2SAdam Hornacek     /**
1562d8a7afe2SAdam Hornacek      * Prepare a search helper with required settings for an internal search.
1563d8a7afe2SAdam Hornacek      * <p>
1564ae1c323bSVladimir Kotal      * NOTE: One should check the {@link SearchHelper#getErrorMsg()} as well as
1565ae1c323bSVladimir Kotal      * {@link SearchHelper#getRedirect()} and take the appropriate action before
1566d8a7afe2SAdam Hornacek      * executing the prepared query or continue processing.
1567d8a7afe2SAdam Hornacek      * <p>
1568d8a7afe2SAdam Hornacek      * This method stops populating fields as soon as an error occurs.
1569d8a7afe2SAdam Hornacek      * @return a search helper.
1570d8a7afe2SAdam Hornacek      */
1571ae1c323bSVladimir Kotal     public SearchHelper prepareInternalSearch(SortOrder sortOrder) {
1572d8a7afe2SAdam Hornacek         String xrValue = req.getParameter(QueryParameters.NO_REDIRECT_PARAM);
1573ae1c323bSVladimir Kotal         return new SearchHelper(getSearchStart(), sortOrder, getDataRoot(), new File(getSourceRootPath()),
1574ae1c323bSVladimir Kotal                 getSearchMaxItems(), getEftarReader(), getQueryBuilder(), getPrefix() == Prefix.SEARCH_R,
1575ae1c323bSVladimir Kotal                 req.getContextPath(), getPrefix() == Prefix.SEARCH_R || getPrefix() == Prefix.SEARCH_P,
1576ae1c323bSVladimir Kotal                 xrValue != null && !xrValue.isEmpty());
1577d8a7afe2SAdam Hornacek     }
1578d8a7afe2SAdam Hornacek 
1579d8a7afe2SAdam Hornacek     /**
1580d8a7afe2SAdam Hornacek      * Get the config w.r.t. the given request. If there is none yet, a new config
1581d8a7afe2SAdam Hornacek      * gets created, attached to the request and returned.
1582d8a7afe2SAdam Hornacek      * <p>
1583d8a7afe2SAdam Hornacek      *
1584d8a7afe2SAdam Hornacek      * @param request the request to use to initialize the config parameters.
1585d8a7afe2SAdam Hornacek      * @return always the same none-{@code null} config for a given request.
1586d8a7afe2SAdam Hornacek      * @throws NullPointerException if the given parameter is {@code null}.
1587d8a7afe2SAdam Hornacek      */
1588d8a7afe2SAdam Hornacek     public static PageConfig get(HttpServletRequest request) {
1589d8a7afe2SAdam Hornacek         Object cfg = request.getAttribute(ATTR_NAME);
1590d8a7afe2SAdam Hornacek         if (cfg != null) {
1591d8a7afe2SAdam Hornacek             return (PageConfig) cfg;
1592d8a7afe2SAdam Hornacek         }
1593d8a7afe2SAdam Hornacek         PageConfig pcfg = new PageConfig(request);
1594d8a7afe2SAdam Hornacek         request.setAttribute(ATTR_NAME, pcfg);
1595d8a7afe2SAdam Hornacek         return pcfg;
1596d8a7afe2SAdam Hornacek     }
1597d8a7afe2SAdam Hornacek 
1598d8a7afe2SAdam Hornacek     private PageConfig(HttpServletRequest req) {
1599d8a7afe2SAdam Hornacek         this.req = req;
1600d8a7afe2SAdam Hornacek         this.authFramework = RuntimeEnvironment.getInstance().getAuthorizationFramework();
1601d8a7afe2SAdam Hornacek         this.executor = RuntimeEnvironment.getInstance().getRevisionExecutor();
1602d8a7afe2SAdam Hornacek         this.fragmentIdentifier = req.getParameter(QueryParameters.FRAGMENT_IDENTIFIER_PARAM);
1603d8a7afe2SAdam Hornacek     }
1604d8a7afe2SAdam Hornacek 
1605d8a7afe2SAdam Hornacek     /**
1606d8a7afe2SAdam Hornacek      * Cleanup all allocated resources (if any) from the instance attached to
1607d8a7afe2SAdam Hornacek      * the given request.
1608d8a7afe2SAdam Hornacek      *
1609d8a7afe2SAdam Hornacek      * @param sr request to check, cleanup. Ignored if {@code null}.
1610d8a7afe2SAdam Hornacek      * @see PageConfig#get(HttpServletRequest)
1611d8a7afe2SAdam Hornacek      */
1612d8a7afe2SAdam Hornacek     public static void cleanup(ServletRequest sr) {
1613d8a7afe2SAdam Hornacek         if (sr == null) {
1614d8a7afe2SAdam Hornacek             return;
1615d8a7afe2SAdam Hornacek         }
1616d8a7afe2SAdam Hornacek         PageConfig cfg = (PageConfig) sr.getAttribute(ATTR_NAME);
1617d8a7afe2SAdam Hornacek         if (cfg == null) {
1618d8a7afe2SAdam Hornacek             return;
1619d8a7afe2SAdam Hornacek         }
1620d8a7afe2SAdam Hornacek         ProjectHelper.cleanup(cfg);
1621d8a7afe2SAdam Hornacek         sr.removeAttribute(ATTR_NAME);
1622d8a7afe2SAdam Hornacek         cfg.env = null;
1623d8a7afe2SAdam Hornacek         cfg.req = null;
1624d8a7afe2SAdam Hornacek         if (cfg.eftarReader != null) {
1625d8a7afe2SAdam Hornacek             cfg.eftarReader.close();
1626d8a7afe2SAdam Hornacek         }
1627d8a7afe2SAdam Hornacek     }
1628d8a7afe2SAdam Hornacek 
1629d8a7afe2SAdam Hornacek     /**
1630d8a7afe2SAdam Hornacek      * Checks if current request is allowed to access project.
1631d8a7afe2SAdam Hornacek      * @param t project
1632d8a7afe2SAdam Hornacek      * @return true if yes
1633d8a7afe2SAdam Hornacek      */
1634d8a7afe2SAdam Hornacek     public boolean isAllowed(Project t) {
1635d8a7afe2SAdam Hornacek         return this.authFramework.isAllowed(this.req, t);
1636d8a7afe2SAdam Hornacek     }
1637d8a7afe2SAdam Hornacek 
1638d8a7afe2SAdam Hornacek     /**
1639d8a7afe2SAdam Hornacek      * Checks if current request is allowed to access group.
1640d8a7afe2SAdam Hornacek      * @param g group
1641d8a7afe2SAdam Hornacek      * @return true if yes
1642d8a7afe2SAdam Hornacek      */
1643d8a7afe2SAdam Hornacek     public boolean isAllowed(Group g) {
1644d8a7afe2SAdam Hornacek         return this.authFramework.isAllowed(this.req, g);
1645d8a7afe2SAdam Hornacek     }
1646d8a7afe2SAdam Hornacek 
1647d8a7afe2SAdam Hornacek 
1648d8a7afe2SAdam Hornacek     public SortedSet<AcceptedMessage> getMessages() {
1649d8a7afe2SAdam Hornacek         return env.getMessages();
1650d8a7afe2SAdam Hornacek     }
1651d8a7afe2SAdam Hornacek 
1652d8a7afe2SAdam Hornacek     public SortedSet<AcceptedMessage> getMessages(String tag) {
1653d8a7afe2SAdam Hornacek         return env.getMessages(tag);
1654d8a7afe2SAdam Hornacek     }
1655d8a7afe2SAdam Hornacek 
1656d8a7afe2SAdam Hornacek     /**
1657d8a7afe2SAdam Hornacek      * Get basename of the path or "/" if the path is empty.
1658d8a7afe2SAdam Hornacek      * This is used for setting title of various pages.
1659d8a7afe2SAdam Hornacek      * @param path path
1660d8a7afe2SAdam Hornacek      * @return short version of the path
1661d8a7afe2SAdam Hornacek      */
1662d8a7afe2SAdam Hornacek     public String getShortPath(String path) {
1663d8a7afe2SAdam Hornacek         File file = new File(path);
1664d8a7afe2SAdam Hornacek 
1665d8a7afe2SAdam Hornacek         if (path.isEmpty()) {
1666d8a7afe2SAdam Hornacek             return "/";
1667d8a7afe2SAdam Hornacek         }
1668d8a7afe2SAdam Hornacek 
1669d8a7afe2SAdam Hornacek         return file.getName();
1670d8a7afe2SAdam Hornacek     }
1671d8a7afe2SAdam Hornacek 
1672d8a7afe2SAdam Hornacek     private String addTitleDelimiter(String title) {
1673d8a7afe2SAdam Hornacek         if (!title.isEmpty()) {
1674d8a7afe2SAdam Hornacek             return title + ", ";
1675d8a7afe2SAdam Hornacek         }
1676d8a7afe2SAdam Hornacek 
1677d8a7afe2SAdam Hornacek         return title;
1678d8a7afe2SAdam Hornacek     }
1679d8a7afe2SAdam Hornacek 
1680d8a7afe2SAdam Hornacek     /**
1681d8a7afe2SAdam Hornacek      * The search page title string should progressively reflect the search terms
1682d8a7afe2SAdam Hornacek      * so that if only small portion of the string is seen, it describes
1683d8a7afe2SAdam Hornacek      * the action as closely as possible while remaining readable.
1684d8a7afe2SAdam Hornacek      * @return string used for setting page title of search results page
1685d8a7afe2SAdam Hornacek      */
1686d8a7afe2SAdam Hornacek     public String getSearchTitle() {
1687d8a7afe2SAdam Hornacek         String title = "";
1688d8a7afe2SAdam Hornacek 
1689d8a7afe2SAdam Hornacek         if (req.getParameter(QueryBuilder.FULL) != null && !req.getParameter(QueryBuilder.FULL).isEmpty()) {
1690d8a7afe2SAdam Hornacek             title += req.getParameter(QueryBuilder.FULL) + " (full)";
1691d8a7afe2SAdam Hornacek         }
1692d8a7afe2SAdam Hornacek         if (req.getParameter(QueryBuilder.DEFS) != null && !req.getParameter(QueryBuilder.DEFS).isEmpty()) {
1693d8a7afe2SAdam Hornacek             title = addTitleDelimiter(title);
1694d8a7afe2SAdam Hornacek             title += req.getParameter(QueryBuilder.DEFS) + " (definition)";
1695d8a7afe2SAdam Hornacek         }
1696d8a7afe2SAdam Hornacek         if (req.getParameter(QueryBuilder.REFS) != null && !req.getParameter(QueryBuilder.REFS).isEmpty()) {
1697d8a7afe2SAdam Hornacek             title = addTitleDelimiter(title);
1698d8a7afe2SAdam Hornacek             title += req.getParameter(QueryBuilder.REFS) + " (reference)";
1699d8a7afe2SAdam Hornacek         }
1700d8a7afe2SAdam Hornacek         if (req.getParameter(QueryBuilder.PATH) != null && !req.getParameter(QueryBuilder.PATH).isEmpty()) {
1701d8a7afe2SAdam Hornacek             title = addTitleDelimiter(title);
1702d8a7afe2SAdam Hornacek             title += req.getParameter(QueryBuilder.PATH) + " (path)";
1703d8a7afe2SAdam Hornacek         }
1704d8a7afe2SAdam Hornacek         if (req.getParameter(QueryBuilder.HIST) != null && !req.getParameter(QueryBuilder.HIST).isEmpty()) {
1705d8a7afe2SAdam Hornacek             title = addTitleDelimiter(title);
1706d8a7afe2SAdam Hornacek             title += req.getParameter(QueryBuilder.HIST) + " (history)";
1707d8a7afe2SAdam Hornacek         }
1708d8a7afe2SAdam Hornacek 
1709d8a7afe2SAdam Hornacek         if (req.getParameterValues(QueryBuilder.PROJECT) != null && req.getParameterValues(QueryBuilder.PROJECT).length != 0) {
1710d8a7afe2SAdam Hornacek             if (!title.isEmpty()) {
1711d8a7afe2SAdam Hornacek                 title += " ";
1712d8a7afe2SAdam Hornacek             }
1713d8a7afe2SAdam Hornacek             title += "in projects: ";
1714d8a7afe2SAdam Hornacek             String[] projects = req.getParameterValues(QueryBuilder.PROJECT);
1715d8a7afe2SAdam Hornacek             title += String.join(",", projects);
1716d8a7afe2SAdam Hornacek         }
1717d8a7afe2SAdam Hornacek 
1718d8a7afe2SAdam Hornacek         return Util.htmlize(title + " - OpenGrok search results");
1719d8a7afe2SAdam Hornacek     }
1720d8a7afe2SAdam Hornacek 
1721d8a7afe2SAdam Hornacek     /**
1722d8a7afe2SAdam Hornacek      * Similar as {@link #getSearchTitle()}.
1723d8a7afe2SAdam Hornacek      * @return string used for setting page title of search view
1724d8a7afe2SAdam Hornacek      */
1725d8a7afe2SAdam Hornacek     public String getHistoryTitle() {
1726d8a7afe2SAdam Hornacek         String path = getPath();
1727379f387bSVladimir Kotal         return Util.htmlize(getShortPath(path) + " - OpenGrok history log for " + path);
1728d8a7afe2SAdam Hornacek     }
1729d8a7afe2SAdam Hornacek 
1730d8a7afe2SAdam Hornacek     public String getPathTitle() {
1731d8a7afe2SAdam Hornacek         String path = getPath();
1732d8a7afe2SAdam Hornacek         String title = getShortPath(path);
1733379f387bSVladimir Kotal         if (!getRequestedRevision().isEmpty()) {
1734d8a7afe2SAdam Hornacek             title += " (revision " + getRequestedRevision() + ")";
1735d8a7afe2SAdam Hornacek         }
1736d8a7afe2SAdam Hornacek         title += " - OpenGrok cross reference for " + (path.isEmpty() ? "/" : path);
1737d8a7afe2SAdam Hornacek 
1738d8a7afe2SAdam Hornacek         return Util.htmlize(title);
1739d8a7afe2SAdam Hornacek     }
1740d8a7afe2SAdam Hornacek 
1741d8a7afe2SAdam Hornacek     public void checkSourceRootExistence() throws IOException {
1742d8a7afe2SAdam Hornacek         if (getSourceRootPath() == null || getSourceRootPath().isEmpty()) {
1743d8a7afe2SAdam Hornacek             throw new FileNotFoundException("Unable to determine source root path. Missing configuration?");
1744d8a7afe2SAdam Hornacek         }
1745d8a7afe2SAdam Hornacek         File sourceRootPathFile = RuntimeEnvironment.getInstance().getSourceRootFile();
1746d8a7afe2SAdam Hornacek         if (!sourceRootPathFile.exists()) {
1747379f387bSVladimir Kotal             throw new FileNotFoundException(String.format("Source root path \"%s\" does not exist",
1748379f387bSVladimir Kotal                     sourceRootPathFile.getAbsolutePath()));
1749d8a7afe2SAdam Hornacek         }
1750d8a7afe2SAdam Hornacek         if (!sourceRootPathFile.isDirectory()) {
1751379f387bSVladimir Kotal             throw new FileNotFoundException(String.format("Source root path \"%s\" is not a directory",
1752379f387bSVladimir Kotal                     sourceRootPathFile.getAbsolutePath()));
1753d8a7afe2SAdam Hornacek         }
1754d8a7afe2SAdam Hornacek         if (!sourceRootPathFile.canRead()) {
1755379f387bSVladimir Kotal             throw new IOException(String.format("Source root path \"%s\" is not readable",
1756379f387bSVladimir Kotal                     sourceRootPathFile.getAbsolutePath()));
1757d8a7afe2SAdam Hornacek         }
1758d8a7afe2SAdam Hornacek     }
1759d8a7afe2SAdam Hornacek 
1760d8a7afe2SAdam Hornacek     /**
1761d8a7afe2SAdam Hornacek      * Get all project related messages. These include
1762d8a7afe2SAdam Hornacek      * <ol>
1763d8a7afe2SAdam Hornacek      * <li>Main messages</li>
1764d8a7afe2SAdam Hornacek      * <li>Messages with tag = project name</li>
1765d8a7afe2SAdam Hornacek      * <li>Messages with tag = project's groups names</li>
1766d8a7afe2SAdam Hornacek      * </ol>
1767d8a7afe2SAdam Hornacek      *
1768379f387bSVladimir Kotal      * @return the sorted set of messages according to accept time
1769d8a7afe2SAdam Hornacek      * @see org.opengrok.indexer.web.messages.MessagesContainer#MESSAGES_MAIN_PAGE_TAG
1770d8a7afe2SAdam Hornacek      */
1771d8a7afe2SAdam Hornacek     private SortedSet<AcceptedMessage> getProjectMessages() {
1772d8a7afe2SAdam Hornacek         SortedSet<AcceptedMessage> messages = getMessages();
1773d8a7afe2SAdam Hornacek 
1774d8a7afe2SAdam Hornacek         if (getProject() != null) {
1775d8a7afe2SAdam Hornacek             messages.addAll(getMessages(getProject().getName()));
1776d8a7afe2SAdam Hornacek             getProject().getGroups().forEach(group -> {
1777d8a7afe2SAdam Hornacek                 messages.addAll(getMessages(group.getName()));
1778d8a7afe2SAdam Hornacek             });
1779d8a7afe2SAdam Hornacek         }
1780d8a7afe2SAdam Hornacek 
1781d8a7afe2SAdam Hornacek         return messages;
1782d8a7afe2SAdam Hornacek     }
1783d8a7afe2SAdam Hornacek 
1784d8a7afe2SAdam Hornacek     /**
1785d8a7afe2SAdam Hornacek      * Decide if this resource has been modified since the header value in the request.
1786d8a7afe2SAdam Hornacek      * <p>
1787d8a7afe2SAdam Hornacek      * The resource is modified since the weak ETag value in the request, the ETag is
1788d8a7afe2SAdam Hornacek      * computed using:
17894a04c503SChris Fraire      *
1790d8a7afe2SAdam Hornacek      * <ul>
1791d8a7afe2SAdam Hornacek      * <li>the source file modification</li>
1792d8a7afe2SAdam Hornacek      * <li>project messages</li>
1793d8a7afe2SAdam Hornacek      * <li>last timestamp for index</li>
1794d8a7afe2SAdam Hornacek      * <li>OpenGrok current deployed version</li>
1795d8a7afe2SAdam Hornacek      * </ul>
1796d8a7afe2SAdam Hornacek      *
1797d8a7afe2SAdam Hornacek      * <p>
1798d8a7afe2SAdam Hornacek      * If the resource was modified, appropriate headers in the response are filled.
17994a04c503SChris Fraire      *
1800d8a7afe2SAdam Hornacek      *
1801d8a7afe2SAdam Hornacek      * @param request the http request containing the headers
1802d8a7afe2SAdam Hornacek      * @param response the http response for setting the headers
1803d8a7afe2SAdam Hornacek      * @return true if resource was not modified; false otherwise
1804d8a7afe2SAdam Hornacek      * @see <a href="https://tools.ietf.org/html/rfc7232#section-2.3">HTTP ETag</a>
1805d8a7afe2SAdam Hornacek      * @see <a href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html">HTTP Caching</a>
1806d8a7afe2SAdam Hornacek      */
1807d8a7afe2SAdam Hornacek     public boolean isNotModified(HttpServletRequest request, HttpServletResponse response) {
1808d8a7afe2SAdam Hornacek         String currentEtag = String.format("W/\"%s\"",
1809d8a7afe2SAdam Hornacek                 Objects.hash(
1810d8a7afe2SAdam Hornacek                         // last modified time as UTC timestamp in millis
1811d8a7afe2SAdam Hornacek                         getLastModified(),
1812d8a7afe2SAdam Hornacek                         // all project related messages which changes the view
1813d8a7afe2SAdam Hornacek                         getProjectMessages(),
1814d8a7afe2SAdam Hornacek                         // last timestamp value
1815d8a7afe2SAdam Hornacek                         getEnv().getDateForLastIndexRun() != null ? getEnv().getDateForLastIndexRun().getTime() : 0,
1816d8a7afe2SAdam Hornacek                         // OpenGrok version has changed since the last time
1817d8a7afe2SAdam Hornacek                         Info.getVersion()
1818d8a7afe2SAdam Hornacek                 )
1819d8a7afe2SAdam Hornacek         );
1820d8a7afe2SAdam Hornacek 
1821d8a7afe2SAdam Hornacek         String headerEtag = request.getHeader(HttpHeaders.IF_NONE_MATCH);
1822d8a7afe2SAdam Hornacek 
1823d8a7afe2SAdam Hornacek         if (headerEtag != null && headerEtag.equals(currentEtag)) {
1824d8a7afe2SAdam Hornacek             // weak ETag has not changed, return 304 NOT MODIFIED
1825d8a7afe2SAdam Hornacek             response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
1826d8a7afe2SAdam Hornacek             return true;
1827d8a7afe2SAdam Hornacek         }
1828d8a7afe2SAdam Hornacek 
1829d8a7afe2SAdam Hornacek         // return 200 OK
1830d8a7afe2SAdam Hornacek         response.setHeader(HttpHeaders.ETAG, currentEtag);
1831d8a7afe2SAdam Hornacek         return false;
1832d8a7afe2SAdam Hornacek     }
1833d8a7afe2SAdam Hornacek 
1834d8a7afe2SAdam Hornacek     /**
1835d8a7afe2SAdam Hornacek      * @param root root path
1836d8a7afe2SAdam Hornacek      * @param path path
1837d8a7afe2SAdam Hornacek      * @return path relative to root
1838d8a7afe2SAdam Hornacek      */
1839d8a7afe2SAdam Hornacek     public static String getRelativePath(String root, String path) {
1840d8a7afe2SAdam Hornacek         return Paths.get(root).relativize(Paths.get(path)).toString();
1841d8a7afe2SAdam Hornacek     }
1842d8a7afe2SAdam Hornacek 
1843d8a7afe2SAdam Hornacek     /**
1844d8a7afe2SAdam Hornacek      * Determines whether a match offset from a search result has been
1845d8a7afe2SAdam Hornacek      * indicated, and if so tries to calculate a translated xref fragment
1846d8a7afe2SAdam Hornacek      * identifier.
1847379f387bSVladimir Kotal      * @return {@code true} if a xref fragment identifier was calculated by the call to this method
1848d8a7afe2SAdam Hornacek      */
1849d8a7afe2SAdam Hornacek     public boolean evaluateMatchOffset() {
1850d8a7afe2SAdam Hornacek         if (fragmentIdentifier == null) {
1851d8a7afe2SAdam Hornacek             int matchOffset = getIntParam(QueryParameters.MATCH_OFFSET_PARAM, -1);
1852d8a7afe2SAdam Hornacek             if (matchOffset >= 0) {
1853d8a7afe2SAdam Hornacek                 File resourceFile = getResourceFile();
1854d8a7afe2SAdam Hornacek                 if (resourceFile.isFile()) {
1855d8a7afe2SAdam Hornacek                     LineBreaker breaker = new LineBreaker();
1856d8a7afe2SAdam Hornacek                     StreamSource streamSource = StreamSource.fromFile(resourceFile);
1857d8a7afe2SAdam Hornacek                     try {
1858d8a7afe2SAdam Hornacek                         breaker.reset(streamSource, in -> ExpandTabsReader.wrap(in, getProject()));
1859d8a7afe2SAdam Hornacek                         int matchLine = breaker.findLineIndex(matchOffset);
1860d8a7afe2SAdam Hornacek                         if (matchLine >= 0) {
1861d8a7afe2SAdam Hornacek                             // Convert to 1-based offset to accord with OpenGrok line number.
1862d8a7afe2SAdam Hornacek                             fragmentIdentifier = String.valueOf(matchLine + 1);
1863d8a7afe2SAdam Hornacek                             return true;
1864d8a7afe2SAdam Hornacek                         }
1865d8a7afe2SAdam Hornacek                     } catch (IOException e) {
1866379f387bSVladimir Kotal                         LOGGER.log(Level.WARNING, String.format("Failed to evaluate match offset for %s",
1867379f387bSVladimir Kotal                                 resourceFile), e);
1868d8a7afe2SAdam Hornacek                     }
1869d8a7afe2SAdam Hornacek                 }
1870d8a7afe2SAdam Hornacek             }
1871d8a7afe2SAdam Hornacek         }
1872d8a7afe2SAdam Hornacek         return false;
1873d8a7afe2SAdam Hornacek     }
1874d8a7afe2SAdam Hornacek }
1875