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