xref: /OpenGrok/opengrok-web/src/main/java/org/opengrok/web/DirectoryListing.java (revision d6df19e1b22784c78f567cf74c42f18e3901b900)
1 /*
2  * CDDL HEADER START
3  *
4  * The contents of this file are subject to the terms of the
5  * Common Development and Distribution License (the "License").
6  * You may not use this file except in compliance with the License.
7  *
8  * See LICENSE.txt included in this distribution for the specific
9  * language governing permissions and limitations under the License.
10  *
11  * When distributing Covered Code, include this CDDL HEADER in each
12  * file and include the License file at LICENSE.txt.
13  * If applicable, add the following below this CDDL HEADER, with the
14  * fields enclosed by brackets "[]" replaced with your own identifying
15  * information: Portions Copyright [yyyy] [name of copyright owner]
16  *
17  * CDDL HEADER END
18  */
19 
20 /*
21  * Copyright (c) 2005, 2021, Oracle and/or its affiliates. All rights reserved.
22  * Portions Copyright (c) 2011, Jens Elkner.
23  * Portions Copyright (c) 2017, 2020, Chris Fraire <cfraire@me.com>.
24  */
25 package org.opengrok.web;
26 
27 import java.io.File;
28 import java.io.IOException;
29 import java.io.Writer;
30 import java.text.Format;
31 import java.text.SimpleDateFormat;
32 import java.util.ArrayList;
33 import java.util.Date;
34 import java.util.List;
35 import java.util.Locale;
36 import java.util.Map;
37 import java.util.logging.Level;
38 import java.util.logging.Logger;
39 import java.util.stream.Collectors;
40 
41 import org.opengrok.indexer.analysis.NullableNumLinesLOC;
42 import org.opengrok.indexer.configuration.PathAccepter;
43 import org.opengrok.indexer.configuration.RuntimeEnvironment;
44 import org.opengrok.indexer.history.HistoryException;
45 import org.opengrok.indexer.history.HistoryGuru;
46 import org.opengrok.indexer.logger.LoggerFactory;
47 import org.opengrok.indexer.search.DirectoryEntry;
48 import org.opengrok.indexer.web.EftarFileReader;
49 import org.opengrok.indexer.web.Util;
50 
51 /**
52  * Generates HTML listing of a Directory.
53  */
54 public class DirectoryListing {
55 
56     private static final Logger LOGGER = LoggerFactory.getLogger(DirectoryListing.class);
57 
58     protected static final String DIRECTORY_SIZE_PLACEHOLDER = "-";
59     private final EftarFileReader desc;
60     private final long now;
61 
DirectoryListing()62     public DirectoryListing() {
63         desc = null;
64         now = System.currentTimeMillis();
65     }
66 
DirectoryListing(EftarFileReader desc)67     public DirectoryListing(EftarFileReader desc) {
68         this.desc = desc;
69         now = System.currentTimeMillis();
70     }
71 
72     /**
73      * Write part of HTML code which contains file/directory last
74      * modification time and size.
75      *
76      * @param out write destination
77      * @param child the file or directory to use for writing the data
78      * @param modTime the time of the last commit that touched {@code child},
79      * or {@code null} if unknown
80      * @param dateFormatter the formatter to use for pretty printing dates
81      *
82      * @throws NullPointerException if a parameter is {@code null}
83      */
printDateSize(Writer out, File child, Date modTime, Format dateFormatter)84     private void printDateSize(Writer out, File child, Date modTime,
85                                Format dateFormatter)
86             throws IOException {
87         long lastm = modTime == null ? child.lastModified() : modTime.getTime();
88 
89         out.write("<td>");
90         if (now - lastm < 86400000) {
91             out.write("Today");
92         } else {
93             out.write(dateFormatter.format(lastm));
94         }
95         out.write("</td><td>");
96         if (child.isDirectory()) {
97             out.write(DIRECTORY_SIZE_PLACEHOLDER);
98         } else {
99             out.write(Util.readableSize(child.length()));
100         }
101         out.write("</td>");
102     }
103 
104     /**
105      * Traverse directory until subdirectory with more than one item
106      * (other than directory) or end of path is reached.
107      * @param dir directory to traverse
108      * @return string representing path with empty directories or the name of the directory
109      */
getSimplifiedPath(File dir)110     private static String getSimplifiedPath(File dir) {
111         String[] files = dir.list();
112 
113         // Permissions can prevent getting list of items in the directory.
114         if (files == null) {
115             return dir.getName();
116         }
117 
118         if (files.length == 1) {
119             File entry = new File(dir, files[0]);
120             PathAccepter pathAccepter = RuntimeEnvironment.getInstance().getPathAccepter();
121             if (pathAccepter.accept(entry) && entry.isDirectory()) {
122                 return (dir.getName() + "/" + getSimplifiedPath(entry));
123             }
124         }
125 
126         return dir.getName();
127     }
128 
129     /**
130      * Calls
131      * {@link #extraListTo(java.lang.String, java.io.File, java.io.Writer, java.lang.String, java.util.List)}
132      * with {@code contextPath}, {@code dir}, {@code out}, {@code path},
133      * and a list mapped from {@code files}.
134      * @param contextPath context path
135      * @param dir directory
136      * @param out writer
137      * @param path path
138      * @param files list of files
139      * @return see
140      * {@link #extraListTo(java.lang.String, java.io.File, java.io.Writer, java.lang.String, java.util.List)}
141      * @throws HistoryException history exception
142      * @throws IOException I/O exception
143      */
listTo(String contextPath, File dir, Writer out, String path, List<String> files)144     public List<String> listTo(String contextPath, File dir, Writer out, String path, List<String> files)
145             throws IOException, HistoryException {
146         List<DirectoryEntry> filesExtra = null;
147         if (files != null) {
148             filesExtra = files.stream().map(f ->
149                 new DirectoryEntry(new File(dir, f), null)).collect(Collectors.toList());
150         }
151         return extraListTo(contextPath, dir, out, path, filesExtra);
152     }
153 
154     /**
155      * Write a HTML-ized listing of the given directory to the given destination.
156      *
157      * @param contextPath path used for link prefixes
158      * @param dir the directory to list
159      * @param out write destination
160      * @param path virtual path of the directory (usually the path name of
161      *  <var>dir</var> with the source root directory stripped off).
162      * @param entries basenames of potential children of the directory to list,
163      *  but filtered by {@link PathAccepter}.
164      * @return a possible empty list of README files included in the written
165      *  listing.
166      * @throws IOException when cannot write to the {@code out} parameter
167      * @throws HistoryException when failed to get last modified time for files in directory
168      */
extraListTo(String contextPath, File dir, Writer out, String path, List<DirectoryEntry> entries)169     public List<String> extraListTo(String contextPath, File dir, Writer out,
170                                     String path, List<DirectoryEntry> entries) throws IOException, HistoryException {
171         // TODO this belongs to a jsp, not here
172         ArrayList<String> readMes = new ArrayList<>();
173         int offset = -1;
174         EftarFileReader.FNode parentFNode = null;
175         if (desc != null) {
176             try {
177                 parentFNode = desc.getNode(path);
178             } catch (IOException e) {
179                 LOGGER.log(Level.WARNING, String.format("cannot get Eftar node for path ''%s''", path), e);
180             }
181             if (parentFNode != null) {
182                 offset = parentFNode.getChildOffset();
183             }
184         }
185 
186         out.write("<table id=\"dirlist\" class=\"tablesorter tablesorter-default\">\n");
187         out.write("<thead>\n");
188         out.write("<tr>\n");
189         out.write("<th class=\"sorter-false\"></th>\n");
190         out.write("<th>Name</th>\n");
191         out.write("<th class=\"sorter-false\"></th>\n");
192         out.write("<th class=\"sort-dates\">Date</th>\n");
193         out.write("<th class=\"sort-groksizes\">Size</th>\n");
194         out.write("<th>#Lines</th>\n");
195         out.write("<th>LOC</th>\n");
196         if (offset > 0) {
197             out.write("<th><samp>Description</samp></th>\n");
198         }
199         out.write("</tr>\n</thead>\n<tbody>\n");
200 
201         PathAccepter pathAccepter = RuntimeEnvironment.getInstance().getPathAccepter();
202         Format dateFormatter = new SimpleDateFormat("dd-MMM-yyyy", Locale.getDefault());
203 
204         // Print the '..' entry even for empty directories.
205         if (path.length() != 0) {
206             out.write("<tr><td><p class=\"'r'\"/></td><td>");
207             out.write("<b><a href=\"..\">..</a></b></td><td></td>");
208             printDateSize(out, dir.getParentFile(), null, dateFormatter);
209             out.write("</tr>\n");
210         }
211 
212         Map<String, Date> modTimes =
213                 HistoryGuru.getInstance().getLastModifiedTimes(dir);
214 
215         if (entries != null) {
216             for (DirectoryEntry entry : entries) {
217                 File child = entry.getFile();
218                 if (!pathAccepter.accept(child)) {
219                     continue;
220                 }
221                 String filename = child.getName();
222                 String filenameLower = filename.toLowerCase(Locale.ROOT);
223                 if (filenameLower.startsWith("readme") ||
224                         filenameLower.endsWith("readme")) {
225                     readMes.add(filename);
226                 }
227                 boolean isDir = child.isDirectory();
228                 out.write("<tr><td>");
229                 out.write("<p class=\"");
230                 out.write(isDir ? 'r' : 'p');
231                 out.write("\"/>");
232                 out.write("</td><td><a href=\"");
233                 if (isDir) {
234                     String longpath = getSimplifiedPath(child);
235                     out.write(Util.uriEncodePath(longpath));
236                     out.write("/\"><b>");
237                     int idx;
238                     if ((idx = longpath.lastIndexOf('/')) > 0) {
239                         out.write("<span class=\"simplified-path\">");
240                         out.write(longpath.substring(0, idx + 1));
241                         out.write("</span>");
242                         out.write(longpath.substring(idx + 1));
243                     } else {
244                         out.write(longpath);
245                     }
246                     out.write("</b></a>/");
247                 } else {
248                     out.write(Util.uriEncodePath(filename));
249                     out.write("\">");
250                     out.write(filename);
251                     out.write("</a>");
252                 }
253                 out.write("</td>");
254                 Util.writeHAD(out, contextPath, path + filename, isDir);
255                 printDateSize(out, child, modTimes.get(filename), dateFormatter);
256                 printNumlines(out, entry, isDir);
257                 printLoc(out, entry, isDir);
258                 if (offset > 0) {
259                     String briefDesc = desc.getChildTag(parentFNode, filename);
260                     if (briefDesc == null) {
261                         out.write("<td/>");
262                     } else {
263                         out.write("<td>");
264                         out.write(briefDesc);
265                         out.write("</td>");
266                     }
267                 }
268                 out.write("</tr>\n");
269             }
270         }
271         out.write("</tbody>\n</table>");
272         return readMes;
273     }
274 
printNumlines(Writer out, DirectoryEntry entry, boolean isDir)275     private void printNumlines(Writer out, DirectoryEntry entry, boolean isDir)
276             throws IOException {
277         Long numlines = null;
278         String readableNumlines = "";
279         NullableNumLinesLOC extra = entry.getExtra();
280         if (extra != null) {
281             numlines = extra.getNumLines();
282         }
283         if (numlines != null) {
284             readableNumlines = Util.readableCount(numlines, isDir);
285         }
286 
287         out.write("<td class=\"numlines\">");
288         out.write(readableNumlines);
289         out.write("</td>");
290     }
291 
printLoc(Writer out, DirectoryEntry entry, boolean isDir)292     private void printLoc(Writer out, DirectoryEntry entry, boolean isDir)
293             throws IOException {
294         Long loc = null;
295         String readableLoc = "";
296         NullableNumLinesLOC extra = entry.getExtra();
297         if (extra != null) {
298             loc = extra.getLOC();
299         }
300         if (loc != null) {
301             readableLoc = Util.readableCount(loc, isDir);
302         }
303 
304         out.write("<td class=\"loc\">");
305         out.write(readableLoc);
306         out.write("</td>");
307     }
308 }
309