/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License (the "License"). * You may not use this file except in compliance with the License. * * See LICENSE.txt included in this distribution for the specific * language governing permissions and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at LICENSE.txt. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END */ /* * Copyright (c) 2011 Jens Elkner. * Copyright (c) 2011, 2012, Oracle and/or its affiliates. All rights reserved. */ package org.opensolaris.opengrok.web; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.security.InvalidParameterException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; import javax.servlet.ServletRequest; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import org.apache.commons.jrcs.diff.Diff; import org.apache.commons.jrcs.diff.DifferentiationFailedException; import org.opensolaris.opengrok.analysis.AnalyzerGuru; import org.opensolaris.opengrok.analysis.ExpandTabsReader; import org.opensolaris.opengrok.analysis.FileAnalyzer.Genre; import org.opensolaris.opengrok.configuration.Configuration; import org.opensolaris.opengrok.configuration.Project; import org.opensolaris.opengrok.configuration.RuntimeEnvironment; import org.opensolaris.opengrok.history.Annotation; import org.opensolaris.opengrok.history.HistoryGuru; import org.opensolaris.opengrok.index.IgnoredNames; import org.opensolaris.opengrok.search.QueryBuilder; import org.opensolaris.opengrok.util.IOUtils; /** * A simple container to lazy initialize common vars wrt. a single request. * It MUST NOT be shared between several requests and * {@link #cleanup(ServletRequest)} should be called before the page context * gets destroyed (e.g.when leaving the {@code service} method). *

* Purpose is to decouple implementation details from web design, so that the * JSP developer does not need to know every implementation detail and normally * has to deal with this class/wrapper, only (so some people may like to call * this class a bean with request scope ;-)). Furthermore it helps to keep the * pages (how content gets generated) consistent and to document the request * parameters used. *

* General contract for this class (i.e. if not explicitly documented): * no method of this class changes neither the request nor the response. * * @author Jens Elkner * @version $Revision$ */ public final class PageConfig { // TODO if still used, get it from the app context boolean check4on = true; private Configuration cfg; private IgnoredNames ignoredNames; private String path; private File resourceFile; private String resourcePath; private EftarFileReader eftarReader; private String sourceRootPath; private Boolean isDir; private String uriEncodedPath; private Prefix prefix; private String pageTitle; private String dtag; private String rev; private Boolean hasAnnotation; private Boolean annotate; private Annotation annotation; private Boolean hasHistory; private SortedSet requestedProjects; private String requestedProjectsString; private List dirFileList; private QueryBuilder queryBuilder; private File dataRoot; private StringBuilder headLines; private static final Logger log = Logger.getLogger(PageConfig.class.getName()); /** * Add the given data to the <head> section of the html page to * generate. * @param data data to add. It is copied as is, so remember to escape * special characters ... */ public void addHeaderData(String data) { if (data == null || data.length() == 0) { return; } if (headLines == null) { headLines = new StringBuilder(); } headLines.append(data); } /** * Get addition data, which should be added as is to the <head> * section of the html page. * @return an empty string if nothing to add, the data otherwise. */ public String getHeaderData() { return headLines == null ? "" : headLines.toString(); } /** * Get all data required to create a diff view wrt. to this request in one go. * @return an instance with just enough information to render a sufficient * view. If not all required parameters were given either they are * supplemented with reasonable defaults if possible, otherwise the * related field(s) are {@code null}. {@link DiffData#errorMsg} * {@code != null} indicates, that an error occured and one should not * try to render a diff view. */ @SuppressWarnings("resource") public DiffData getDiffData() { DiffData data = new DiffData(); data.file = new String[2][]; String srcRoot = getSourceRootPath(); String context = req.getContextPath(); String[] rev = new String[2]; String[] filepath = new String[2]; for (int i = 1; i <= 2; i++) { String[] tmp = null; String p = req.getParameter("r" + i); if (p != null) { tmp = p.split("@"); } if (tmp != null && tmp.length == 2) { filepath[i - 1] = tmp[0]; rev[i - 1] = tmp[1]; } } data.fp1 = filepath[0]; data.fp2 = filepath[1]; if (rev[0] == null || rev[1] == null || rev[0].length() == 0 || rev[1].length() == 0 || rev[0].equals(rev[1])) { data.errorMsg = "Please pick two revisions to compare the changed " + "from the history"; return data; } data.rev1 = rev[0]; data.rev2 = rev[1]; InputStream[] in = new InputStream[2]; BufferedReader br = null; Genre[] genre = new Genre[2]; // would be nice, if history db could store genre as well, so that one // does not need to fetch undiffable binary files ... try { for (int i = 0; i < 2; i++) { File f = new File(srcRoot + filepath[i]); in[i] = HistoryGuru.getInstance() .getRevision(f.getParent(), f.getName(), rev[i]); if (in[i] == null) { data.errorMsg = "Unable to get revision " + rev[i] + " for file '" + Util.htmlize(filepath[i]) + "'."; return data; } try { genre[i] = AnalyzerGuru.getGenre(in[0]); } catch (IOException e) { data.errorMsg = "Error when trying to determine the file type of " + Util.htmlize(filepath[i] + " revision " + rev[i] + ": " + e.getMessage()); return data; } if (genre[i] == null) { data.errorMsg = "Unable to determine the file type of " + Util.htmlize(filepath[i] + " revision " + rev[i] + "."); return data; } } if (genre[0] != genre[1]) { data.errorMsg = "Diffs between files of different type (" + genre[0] + " vs. " + genre[1] + ") are not supported."; return data; } data.genre = genre[0]; if (data.genre != Genre.PLAIN && data.genre != Genre.HTML) { return data; } // get the diffs ArrayList lines = new ArrayList(); Project p = getProject(); for (int i = 0; i < 2; i++) { br = new BufferedReader(ExpandTabsReader.wrap(new InputStreamReader(in[i]), p)); String line; while ((line = br.readLine()) != null) { lines.add(line); } data.file[i] = lines.toArray(new String[lines.size()]); lines.clear(); IOUtils.close(br); in[i] = null; br = null; } } catch (Exception e) { data.errorMsg = "Error reading revisions: " + Util.htmlize(e.getMessage()); } finally { IOUtils.close(br); for (int i = 0; i < 2; i++) { IOUtils.close(in[i]); } } if (data.errorMsg != null) { return data; } try { data.revision = Diff.diff(data.file[0], data.file[1]); } catch (DifferentiationFailedException e) { data.errorMsg = "Unable to get diffs: " + Util.htmlize(e.getMessage()); } String[] dd = new String[2]; for (int i = 0; i < 2; i++) { dd[i] = Util.uriEncodeQueryValue(filepath[i] + '@' + rev[i]); } data.rp1 = dd[0]; data.rp2 = dd[1]; data.full = fullDiff(); data.type = getDiffType(); data.reqURI = req.getRequestURI(); return data; } /** * Get the diff display type to use wrt. the request parameter {@code format}. * @return {@link DiffType#SIDEBYSIDE} if the request contains no such parameter * or one with an unknown value, the recognized diff type otherwise. * @see DiffType#get(String) * @see DiffType#getAbbrev() * @see DiffType#toString() */ public DiffType getDiffType() { DiffType d = DiffType.get(req.getParameter("format")); return d == null ? DiffType.SIDEBYSIDE : d; } /** * Check, whether a full diff should be displayed. * @return {@code true} if a request parameter {@code full} with the * literal value {@code 1} was found. */ public boolean fullDiff() { String val = req.getParameter("full"); return val != null && val.equals("1"); } /** * Check, whether the request contains minimal information required to * produce a valid page. If this method returns an empty string, the * referred file or directory actually exists below the source root * directory and is readable. * * @return {@code null} if the referred src file, directory or history is not * available, an empty String if further processing is ok and a non-empty * string which contains the URI encoded redirect path if the request * should be redirected. * @see #getRequestedRevision() * @see #getOnRedirect() * @see #getDirectoryRedirect() * @see #isDir() * @see #hasHistory() */ public String canProcess() { // (1) if it is a history request, the requested resource does not need to // exist in the current tree anymore // (2) diff could always involve virtual files - let it handle by itself // (3) simalar to (1): if a revision is requested, the file does not need // to exist anymore if ((getPrefix() == Prefix.HIST_L && hasHistory()) || prefix == Prefix.DIFF_P || ((prefix == Prefix.XREF_P || prefix == Prefix.RAW_P) && !isDir() && !getRequestedRevision().isEmpty())) { return ""; } if (ignorePath(getPath())) { return null; } if (getResourcePath().equals("/")) { String s = getOnRedirect(); if (s != null) { return s; } if (!isDir()) { // files need to have a revision param and would be handled in // (3) above return null; } } // most links are relative, so we need a trailing slash for dirs // otherwise a link like http://.../source/xref + $rLink would result // into http://.../source/$rLink (last part gets stripped off) String redir = getDirectoryRedirect(); if (redir == null) { if (path.isEmpty() && prefix == Prefix.XREF_P) { return ""; } if ((prefix == Prefix.HIST_L && !hasHistory()) || (prefix == Prefix.XREF_P && getResourcePath().equals("/") && !hasHistory())) { return null; } } // jel: outfactored from list.jsp - seems to be bogus if (isDir()) { if (prefix == Prefix.XREF_P) { if (getResourcePath().equals("/")) { // virtual - isDir already made the dir history check return ""; } if (getResourceFileList().isEmpty() && !getRequestedRevision().isEmpty() && !hasHistory()) { return null; } } else if (getPrefix() == Prefix.RAW_P) { return null; } } return redir == null ? "" : redir; } /** * Get a list of filenames in the requested path. * @return an empty list, if the resource does not exist, is not a * directory or an error occurred when reading it, otherwise a list of * filenames in that directory, sorted alphabetically * @see #getResourceFile() * @see #isDir() */ public List getResourceFileList() { if (dirFileList == null) { String[] files = null; if (isDir() && getResourcePath().length() > 1) { files = getResourceFile().list(); } if (files == null) { dirFileList = Collections.emptyList(); } else { Arrays.sort(files, String.CASE_INSENSITIVE_ORDER); dirFileList = Collections.unmodifiableList(Arrays.asList(files)); } } return dirFileList; } /** * Get the time of last modification of the related file or directory. * @return the last modification time of the related file or directory. * @see File#lastModified() */ public long getLastModified() { return getResourceFile().lastModified(); } /** * Get all RSS related directories from the request using its {@code also} * parameter. * @return an empty string if the requested resource is not a directory, a * space (' ') separated list of unchecked directory names otherwise. */ public String getHistoryDirs() { if (!isDir()) { return ""; } String[] val = req.getParameterValues("also"); if (val == null || val.length == 0) { return path; } StringBuilder paths = new StringBuilder(path); for (int i = 0; i < val.length; i++) { paths.append(' ').append(val[i]); } return paths.toString(); } /** * Get the int value of the given request parameter. * @param name name of the parameter to lookup. * @param defaultValue value to return, if the parameter is not set, is not * a number, or is < 0. * @return the parsed int value on success, the given default value otherwise. */ public int getIntParam(String name, int defaultValue) { int ret = defaultValue; String s = req.getParameter(name); if (s != null && s.length() != 0) { try { int x = Integer.parseInt(s, 10); if (x >= 0) { ret = x; } } catch (Exception e) { log.info("Failed to parse integer " + s); log.log(Level.FINE, "getIntParam", e); } } return ret; } /** * Get the start index for a search result to return by looking up * the {@code start} request parameter. * @return 0 if the corresponding start parameter is not set or not a number, * the number found otherwise. */ public int getSearchStart() { return getIntParam("start", 0); } /** * Get the number of search results to max. return by looking up the * {@code n} request parameter. * * @return the default number of hits if the corresponding start parameter * is not set or not a number, the number found otherwise. */ public int getSearchMaxItems() { return getIntParam("n", getConfig().getHitsPerPage()); } /** * Get sort orders from the request parameter {@code sort} and if this list * would be empty from the cookie {@code OpenGrokorting}. * @return a possible empty list which contains the sort order values in * the same order supplied by the request parameter or cookie(s). */ public List getSortOrder() { List sort = new ArrayList(); List vals = getParamVals("sort"); for (String s : vals) { SortOrder so = SortOrder.get(s); if (so != null) { sort.add(so); } } if (sort.isEmpty()) { vals = getCookieVals("OpenGrokSorting"); for (String s : vals) { SortOrder so = SortOrder.get(s); if (so != null) { sort.add(so); } } } return sort; } /** * Get a reference to the {@code QueryBuilder} wrt. to the current request * parameters: *

*
q
*
freetext lookup rules
*
defs
*
definitions lookup rules
*
path
*
path related rules
*
hist
*
history related rules
*
* @return a query builder with all relevant fields populated. */ public QueryBuilder getQueryBuilder() { if (queryBuilder == null) { queryBuilder = new QueryBuilder().setFreetext(req.getParameter("q")).setDefs(req.getParameter("defs")).setRefs(req.getParameter("refs")).setPath(req.getParameter("path")).setHist(req.getParameter("hist")); // This is for backward compatibility with links created by OpenGrok // 0.8.x and earlier. We used to concatenate the entire query into a // single string and send it in the t parameter. If we get such a // link, just add it to the freetext field, and we'll get the old // behaviour. We can probably remove this code in the first feature // release after 0.9. String t = req.getParameter("t"); if (t != null) { queryBuilder.setFreetext(t); } } return queryBuilder; } /** * Get the eftar reader for the opengrok data directory. If it has been * already opened and not closed, this instance gets returned. One should * not close it once used: {@link #cleanup(ServletRequest)} takes care to * close it. * * @return {@code null} if a reader can't be established, the reader * otherwise. */ public EftarFileReader getEftarReader() { if (eftarReader == null || eftarReader.isClosed()) { File f = getConfig().getDtagsEftar(); if (f == null) { eftarReader = null; } else { try { eftarReader = new EftarFileReader(f); } catch (Exception e) { log.log(Level.FINE, "Failed to create EftarFileReader", e); } } } return eftarReader; } /** * Get the definition tag for the request related file or directory. * @return an empty string if not found, the tag otherwise. */ public String getDefineTagsIndex() { if (dtag != null) { return dtag; } getEftarReader(); if (eftarReader != null) { try { dtag = eftarReader.get(getPath()); // cfg.getPrefix() != Prefix.XREF_S) { } catch (IOException e) { log.info("Failed to get entry from eftar reader: " + e.getMessage()); log.log(Level.FINE, "getDefineTagsIndex", e); } } if (dtag == null) { dtag = ""; } return dtag; } /** * Get the revision parameter {@code r} from the request. * @return {@code "r=revision"} if found, an empty string otherwise. */ public String getRequestedRevision() { if (rev == null) { String tmp = req.getParameter("r"); rev = (tmp != null && tmp.length() > 0) ? "r=" + tmp : ""; } return rev; } /** * Check, whether the request related resource has history information. * @return {@code true} if history is available. * @see HistoryGuru#hasHistory(File) */ public boolean hasHistory() { if (hasHistory == null) { hasHistory = Boolean.valueOf(HistoryGuru.getInstance() .hasHistory(new File(getSourceRootPath(), getPath()))); } return hasHistory.booleanValue(); } /** * Check, whether annotations are available for the related resource. * @return {@code true} if annotions are available. */ public boolean hasAnnotations() { if (hasAnnotation == null) { hasAnnotation = Boolean.valueOf(!isDir() && HistoryGuru.getInstance().hasHistory(getResourceFile())); } return hasAnnotation.booleanValue(); } /** * Check, whether the resource to show should be annotated. * @return {@code true} if annotation is desired and available. */ public boolean annotate() { if (annotate == null) { annotate = Boolean.valueOf(hasAnnotations() // TODO: remove ?a=true support? && (req.getServletPath().startsWith("/json/") || Boolean.parseBoolean(req.getParameter("a")))); } return annotate.booleanValue(); } /** * Get the annotation for the reqested resource (revision parameter is * automagically honored). * @return {@code null} if not available or annotation was not requested, * the cached annotation otherwise. * @see #getRequestedRevision() */ public Annotation getAnnotation() { if (isDir() || getResourcePath().equals("/") || !annotate()) { return null; } if (annotation != null) { return annotation; } getRequestedRevision(); try { annotation = HistoryGuru.getInstance() .annotate(resourceFile, rev.isEmpty() ? null : rev.substring(2)); } catch (IOException e) { log.warning("Failed to get annotations: " + e.getMessage()); log.log(Level.FINE, "getAnnotation", e); /* ignore */ } return annotation; } /** * Get the name which should be shown as "Crossfile" * @return the name of the related file or directory. */ public String getCrossFilename() { return getResourceFile().getName(); } /** * Get the {@code path} parameter and display value for "Search only in" * option. * @return always an array of 3 fields, whereby field[0] contains the * htmlized path value to use (starts and ends always with a '/'). Field[1] * is set to {@code disabled=""} if the current path is the "/" directory, * otherwise set to an empty string. */ public String[] getSearchOnlyIn() { return path.length() == 0 ? new String[] { "/", "disabled=\"\""} : new String[] { isDir() ? Util.htmlize(path) : Util.htmlize(path.substring(0, path.lastIndexOf('/') + 1)), "" }; } /** * Get the project {@link #getPath()} refers to. * @return {@code null} if not available, the project otherwise. */ public Project getProject() { return Project.getProject(getResourceFile()); } /** * Same as {@link #getRequestedProjects()} but returns the project names as * a coma separated String. * @return a possible empty String but never {@code null}. */ public String getRequestedProjectsAsString() { if (requestedProjectsString == null) { Set projects = getRequestedProjects(); if (projects.isEmpty()) { requestedProjectsString = ""; } else { StringBuilder buf = new StringBuilder(); for (String name : projects) { buf.append(name).append(','); } buf.setLength(buf.length() - 1); requestedProjectsString = buf.toString(); } } return requestedProjectsString; } /** * Get the document hash provided by the request parameter {@code h}. * @return {@code null} if the request does not contain such a parameter, * its value otherwise. */ public String getDocumentHash() { return req.getParameter("h"); } /** * Get a reference to a set of requested projects via request parameter * {@code project} or cookies or defaults. *

* NOTE: This method assumes, that project names do not contain * a comma (','), since this character is used as name separator! * * @return a possible empty set of project names aka descriptions but never * {@code null}. It is determined as * follows: *

    *
  1. If there is no project in the runtime environment (RTE) an empty * set is returned. Otherwise:
  2. *
  3. If there is only one project in the RTE, this one gets returned (no * matter, what the request actually says). Otherwise
  4. *
  5. If the request parameter {@code project} contains any available * project, the set with invalid projects removed gets returned. * Otherwise:
  6. *
  7. If the request has a cookie with the name {@code OpenGrokProject} * and it contains any available project, the set with invalid * projects removed gets returned. Otherwise:
  8. *
  9. If a default project is set in the RTE, this project gets returned. * Otherwise:
  10. *
  11. an empty set
  12. *
*/ public SortedSet getRequestedProjects() { if (requestedProjects == null) { requestedProjects = getRequestedProjects("project", "OpenGrokProject"); } return requestedProjects; } private static Pattern COMMA_PATTERN = Pattern.compile(","); private static void splitByComma(String value, List result) { if (value == null || value.length() == 0) { return; } String p[] = COMMA_PATTERN.split(value); for (int k = 0; k < p.length; k++) { if (p[k].length() != 0) { result.add(p[k]); } } } /** * Get the cookie values for the given name. Splits comma separated values * automatically into a list of Strings. * @param cookieName name of the cookie. * @return a possible empty list. */ public List getCookieVals(String cookieName) { Cookie[] cookies = req.getCookies(); ArrayList res = new ArrayList(); if (cookies != null) { for (int i = cookies.length - 1; i >= 0; i--) { if (cookies[i].getName().equals(cookieName)) { splitByComma(cookies[i].getValue(), res); } } } return res; } /** * Get the parameter values for the given name. Splits comma separated * values automatically into a list of Strings. * @param name name of the parameter. * @return a possible empty list. */ private List getParamVals(String paramName) { String vals[] = req.getParameterValues(paramName); List res = new ArrayList(); if (vals != null) { for (int i = vals.length - 1; i >= 0; i--) { splitByComma(vals[i], res); } } return res; } /** * Same as {@link #getRequestedProjects()}, but with a variable cookieName * and parameter name. This way it is trivial to implement a project filter * ... * @param paramName the name of the request parameter, which possibly * contains the project list in question. * @param cookieName name of the cookie which possible contains project * lists used as fallback * @return a possible empty set but never {@code null}. */ protected SortedSet getRequestedProjects(String paramName, String cookieName) { TreeSet set = new TreeSet(); List projects = getConfig().getProjects(); if (projects == null) { return set; } if (projects.size() == 1) { set.add(projects.get(0).getDescription()); return set; } List vals = getParamVals(paramName); for (String s : vals) { if (Project.getByDescription(s) != null) { set.add(s); } } if (set.isEmpty()) { List cookies = getCookieVals(cookieName); for (String s : cookies) { if (Project.getByDescription(s) != null) { set.add(s); } } } if (set.isEmpty()) { Project defaultProject = cfg.getDefaultProject(); if (defaultProject != null) { set.add(defaultProject.getDescription()); } } return set; } /** * Set the page title to use. * @param title title to set (might be {@code null}). */ public void setTitle(String title) { pageTitle = title; } /** * Get the page title to use. * @return {@code null} if not set, the page title otherwise. */ public String getTitle() { return pageTitle; } /** * Get the base path to use to refer to CSS stylesheets and related * resources. Usually used to create links. * * @return the appropriate application directory prefixed with the * application's context path (e.g. "/source/default"). * @see HttpServletRequest#getContextPath() * @see Configuration#getWebappLAF() */ public String getCssDir() { return req.getContextPath() + '/' + getConfig().getWebappLAF(); } /** * Get the current runtime environment. * * @return the runtime env. * @see RuntimeEnvironment#getInstance() * @see RuntimeEnvironment#register() */ public Configuration getConfig() { if (cfg == null) { RuntimeEnvironment.register(); cfg = RuntimeEnvironment.getConfig(); } return cfg; } /** * Get the name patterns used to determine, whether a file should be * ignored. * * @return the corresponding value from the current runtime config.. */ public IgnoredNames getIgnoredNames() { if (ignoredNames == null) { ignoredNames = getConfig().getIgnoredNames(); } return ignoredNames; } /** * Get the canonical path to root of the source tree. File separators are * replaced with a '/'. * * @return The on disk source root directory. * @see Configuration#getSourceRoot() */ public String getSourceRootPath() { if (sourceRootPath == null) { sourceRootPath = getConfig().getSourceRoot().replace(File.separatorChar, '/'); } return sourceRootPath; } /** * Get the prefix for the related request. * @return {@link Prefix#UNKNOWN} if the servlet path matches any known * prefix, the prefix otherwise. */ public Prefix getPrefix() { if (prefix == null) { prefix = Prefix.get(req.getServletPath()); } return prefix; } /** * Get the canonical path of the related resource relative to the * source root directory (used file separators are all '/'). No check is * made, whether the obtained path is really an accessible resource on disk. * * @see HttpServletRequest#getPathInfo() * @return a possible empty String (denotes the source root directory) but * not {@code null}. */ public String getPath() { if (path == null) { path = Util.getCanonicalPath(req.getPathInfo(), '/'); if ("/".equals(path)) { path = ""; } } return path; } /** * If a requested resource is not available, append "/on/" to * the source root directory and try again to resolve it. * * @return on success a none-{@code null} gets returned, which should be * used to redirect the client to the propper path. */ public String getOnRedirect() { if (check4on) { File newFile = new File(getSourceRootPath() + "/on/" + getPath()); if (newFile.canRead()) { return req.getContextPath() + req.getServletPath() + "/on" + getUriEncodedPath() + (newFile.isDirectory() ? trailingSlash(path) : ""); } } return null; } /** * Get the on disk file to the request related file or directory. * * NOTE: If a repository contains hard or symbolic links, the returned * file may finally point to a file outside of the source root directory. * * @return {@code new File("/")} if the related file or directory is not * available (can not be find below the source root directory), * the readable file or directory otherwise. * @see #getSourceRootPath() * @see #getPath() */ public File getResourceFile() { if (resourceFile == null) { resourceFile = new File(getSourceRootPath(), getPath()); if (!resourceFile.canRead()) { resourceFile = new File("/"); } } return resourceFile; } /** * Get the canonical on disk path to the request related file or directory * with all file separators replaced by a '/'. * * @return "/" if the evaluated path is invalid or outside the source root * directory), otherwise the path to the readable file or directory. * @see #getResourceFile() */ public String getResourcePath() { if (resourcePath == null) { resourcePath = getResourceFile().getPath().replace(File.separatorChar, '/'); } return resourcePath; } /** * Check, whether the given path or its parent should be ignored. * @param path path to check * @return {@code true} if it should be ignored. * @see #getIgnoredNames() * @see IgnoredNames */ public boolean ignorePath(String path) { if (path == null) { return true; } getIgnoredNames(); if (ignoredNames.match(path)) { return true; } int idx = path.lastIndexOf('/'); return idx != -1 && ignoredNames.match(path.substring(0, idx)); } /** * Check, whether the related request resource matches a valid file or * directory below the source root directory and wether it matches an * ignored pattern. * * @return {@code true} if the related resource does not exists or should be * ignored. * @see #getResourcePath() * @see #getPath() * @see #ignorePath(String) */ public boolean resourceNotAvailable() { return getResourcePath().equals("/") || ignorePath(getPath()); } /** * Check, whether the request related path represents a directory. * * @return {@code true} if directory related request */ public boolean isDir() { if (isDir == null) { if (getPrefix() == Prefix.DIFF_P) { isDir = Boolean.FALSE; // diff is stable enough to handle it } else if ((prefix == Prefix.HIST_L || prefix == Prefix.XREF_P || prefix == Prefix.RAW_P) && getResourcePath().equals("/")) // does not exist [anymore] { // doesn't exist in the current tree so ask the guru isDir = HistoryGuru.getInstance() .isDirectory(getSourceRootPath() + getPath()); if (isDir == null) { isDir = Boolean.FALSE; } } else { isDir = Boolean.valueOf(getResourceFile().isDirectory()); } } return isDir.booleanValue(); } private static String trailingSlash(String path) { return path.length() == 0 || path.charAt(path.length() - 1) != '/' ? "/" : ""; } private File checkFile(File dir, String name) { File src = isDir() ? new File(resourceFile, name) : resourceFile; File f = new File(dir, name); if (f.exists() && f.isFile() && f.lastModified() >= src.lastModified()) { return f; } return null; } /** * Find the files with the given names in the {@link #getPath()} directory * relative to the crossfile directory of the opengrok data directory. * * @param filenames filenames to lookup. * @return an empty array if the related directory does not exist or the * given list is {@code null} or empty, otherwise an array, which may * contain {@code null} entries (when the related file could not be found * or the related source file is younger) having the same order as the * given list. */ public File[] findDataFiles(List filenames) { if (filenames == null || filenames.isEmpty()) { return new File[0]; } File[] res = new File[filenames.size()]; File dir = new File(getConfig().getDataRoot() + Prefix.XREF_P + path); if (dir.exists() && dir.isDirectory()) { getResourceFile(); for (int i = res.length - 1; i >= 0; i--) { res[i] = checkFile(dir, filenames.get(i)); } } return res; } /** * Lookup the file {@link #getPath()} relative to the crossfile directory * of the opengrok data directory. * @return {@code null} if not found or the related source file is younger * than the crossfile, the crossfile otherwise. */ public File findDataFile() { return checkFile(new File(getConfig().getDataRoot() + Prefix.XREF_P), path); } /** * Get the path the request should be redirected (if any). * * @return {@code null} if there is no reason to redirect, the URI encoded * redirect path to use otherwise. */ public String getDirectoryRedirect() { if (isDir()) { if (req.getPathInfo() != null && path.length() == 0) { // => /, but not /$prefix without trailing slash return null; } getPrefix(); if (prefix != Prefix.XREF_P && prefix != Prefix.HIST_L) { //if it is an existing dir perhaps people wanted dir xref return req.getContextPath() + Prefix.XREF_P + getUriEncodedPath() + trailingSlash(path); } String ts = trailingSlash(path); if (ts.length() != 0) { return req.getContextPath() + prefix + getUriEncodedPath() + ts; } } return null; } /** * Get the URI encoded canonical path to the related file or directory * (the URI part between the servlet path and the start of the query string). * @return an URI encoded path which might be an empty string but not * {@code null}. * @see #getPath() */ public String getUriEncodedPath() { if (uriEncodedPath == null) { uriEncodedPath = Util.uriEncodePath(getPath()); } return uriEncodedPath; } /** * Get opengrok's configured dataroot directory. * It is veriefied, that the used environment has a valid opengrok data root * set and that it is an accessable directory. * @return the opengrok data directory. * @throws InvalidParameterException if inaccessable or not set. */ public File getDataRoot() { if (dataRoot == null) { String tmp = getConfig().getDataRoot(); if (tmp == null || tmp.length() == 0) { throw new InvalidParameterException("dataRoot parameter is not " + "set in configuration.xml!"); } dataRoot = new File(tmp); if (!(dataRoot.isDirectory() && dataRoot.canRead())) { throw new InvalidParameterException("The configured dataRoot '" + tmp + "' refers to a none-exsting or unreadable directory!"); } } return dataRoot; } /** * Prepare a search helper with all required information, ready to execute * the query implied by the related request parameters and cookies. *

* NOTE: One should check the {@link SearchHelper#errorMsg} as well as * {@link SearchHelper#redirect} and take the appropriate action before * executing the prepared query or continue processing. *

* This method stops populating fields as soon as an error occurs. * * @return a search helper. */ public SearchHelper prepareSearch() { SearchHelper sh = new SearchHelper(); sh.dataRoot = getDataRoot(); // throws Exception if none-existent List sortOrders = getSortOrder(); sh.order = sortOrders.isEmpty() ? SortOrder.RELEVANCY : sortOrders.get(0); if (getRequestedProjects().isEmpty() && getConfig().hasProjects()) { sh.errorMsg = "You must select a project!"; return sh; } sh.builder = getQueryBuilder(); if (sh.builder.getSize() == 0) { // Entry page show the map sh.redirect = req.getContextPath() + '/'; return sh; } sh.start = getSearchStart(); sh.maxItems = getSearchMaxItems(); sh.contextPath = req.getContextPath(); // jel: this should be IMHO a config param since not only core dependend sh.parallel = Runtime.getRuntime().availableProcessors() > 1; sh.isCrossRefSearch = getPrefix() == Prefix.SEARCH_R; sh.desc = getEftarReader(); sh.sourceRoot = new File(getSourceRootPath()); return sh; } /** * Get the config wrt. the given request. If there is none yet, a new config * gets created, attached to the request and returned. *

* * @param request the request to use to initialize the config parameters. * @return always the same none-{@code null} config for a given request. * @throws NullPointerException * if the given parameter is {@code null}. */ public static PageConfig get(HttpServletRequest request) { Object cfg = request.getAttribute(ATTR_NAME); if (cfg != null) { return (PageConfig) cfg; } PageConfig pcfg = new PageConfig(request); request.setAttribute(ATTR_NAME, pcfg); return pcfg; } private static final String ATTR_NAME = PageConfig.class.getCanonicalName(); private HttpServletRequest req; private PageConfig(HttpServletRequest req) { this.req = req; } /** * Cleanup all allocated resources (if any) from the instance attached to * the given request. * @param sr request to check, cleanup. Ignored if {@code null}. * @see PageConfig#get(HttpServletRequest) * */ public static void cleanup(ServletRequest sr) { if (sr == null) { return; } PageConfig cfg = (PageConfig) sr.getAttribute(ATTR_NAME); if (cfg == null) { return; } sr.removeAttribute(ATTR_NAME); cfg.cfg = null; if (cfg.eftarReader != null) { cfg.eftarReader.close(); } } }