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) 2007, 2018, Oracle and/or its affiliates. All rights reserved. 22 * Portions Copyright (c) 2017, Chris Fraire <cfraire@me.com>. 23 */ 24 package org.opengrok.web; 25 26 import java.io.ByteArrayInputStream; 27 import java.io.File; 28 import java.io.FileOutputStream; 29 import java.io.StringWriter; 30 import java.nio.file.Files; 31 import java.text.SimpleDateFormat; 32 import java.util.Arrays; 33 import java.util.List; 34 import javax.xml.parsers.DocumentBuilder; 35 import javax.xml.parsers.DocumentBuilderFactory; 36 import org.junit.After; 37 import org.junit.Before; 38 import org.junit.Test; 39 import org.opengrok.indexer.configuration.RuntimeEnvironment; 40 import org.opengrok.indexer.history.RepositoryFactory; 41 import org.w3c.dom.Document; 42 import org.w3c.dom.Element; 43 import org.w3c.dom.Node; 44 import org.w3c.dom.NodeList; 45 46 import static org.junit.Assert.*; 47 48 /** 49 * JUnit test to test that the DirectoryListing produce the expected result 50 */ 51 public class DirectoryListingTest { 52 53 /** 54 * Indication of that the file was a directory and so that the size given by 55 * the FS is platform dependent. 56 */ 57 private static final int DIRECTORY_INTERNAL_SIZE = -2; 58 /** 59 * Indication of unparseable file size. 60 */ 61 private static final int INVALID_SIZE = -1; 62 63 private File directory; 64 private FileEntry[] entries; 65 private SimpleDateFormat dateFormatter; 66 67 class FileEntry implements Comparable<FileEntry> { 68 69 String name; 70 String href; 71 long lastModified; 72 /** 73 * May be: 74 * <pre> 75 * positive integer - for a file 76 * -2 - for a directory 77 * -1 - for an unparseable size 78 * </pre> 79 */ 80 int size; 81 List<FileEntry> subdirs; 82 FileEntry()83 FileEntry() { 84 dateFormatter = new SimpleDateFormat("dd-MMM-yyyy"); 85 } 86 FileEntry(String name, String href, long lastModified, int size, List<FileEntry> subdirs)87 private FileEntry(String name, String href, long lastModified, int size, List<FileEntry> subdirs) { 88 this(); 89 this.name = name; 90 this.href = href; 91 this.lastModified = lastModified; 92 this.size = size; 93 this.subdirs = subdirs; 94 } 95 96 /** 97 * Creating the directory entry. 98 * 99 * @param name name of the file 100 * @param href href to the file 101 * @param lastModified date of last modification 102 * @param subdirs list of sub entries (may be empty) 103 */ FileEntry(String name, String href, long lastModified, List<FileEntry> subdirs)104 FileEntry(String name, String href, long lastModified, List<FileEntry> subdirs) { 105 this(name, href, lastModified, DIRECTORY_INTERNAL_SIZE, subdirs); 106 assertNotNull(subdirs); 107 } 108 109 /** 110 * Creating a regular file entry. 111 * 112 * @param name name of the file 113 * @param href href to the file 114 * @param lastModified date of last modification 115 * @param size the desired size of the file on the disc 116 */ FileEntry(String name, String href, long lastModified, int size)117 FileEntry(String name, String href, long lastModified, int size) { 118 this(name, href, lastModified, size, null); 119 } 120 create()121 private void create() throws Exception { 122 File file = new File(directory, name); 123 124 if (subdirs != null && subdirs.size() > 0) { 125 // this is a directory 126 assertTrue("Failed to create a directory", file.mkdirs()); 127 for (FileEntry entry : subdirs) { 128 entry.name = name + File.separator + entry.name; 129 entry.create(); 130 } 131 } else { 132 assertTrue("Failed to create file", file.createNewFile()); 133 } 134 135 long val = lastModified; 136 if (val == Long.MAX_VALUE) { 137 val = System.currentTimeMillis(); 138 } 139 140 assertTrue("Failed to set modification time", 141 file.setLastModified(val)); 142 143 if (subdirs == null && size > 0) { 144 try (FileOutputStream out = new FileOutputStream(file)) { 145 byte[] buffer = new byte[size]; 146 out.write(buffer); 147 } 148 } 149 } 150 151 @Override compareTo(FileEntry fe)152 public int compareTo(FileEntry fe) { 153 int ret = -1; 154 155 // @todo verify all attributes! 156 if (name.compareTo(fe.name) == 0 157 && href.compareTo(fe.href) == 0) { 158 if ( // this is a file so the size must be exact 159 (subdirs == null && size == fe.size) 160 // this is a directory so the size must have been "-" char 161 || (subdirs != null && size == DIRECTORY_INTERNAL_SIZE)) { 162 ret = 0; 163 } 164 } 165 return ret; 166 } 167 } 168 169 @Before setUp()170 public void setUp() throws Exception { 171 directory = Files.createTempDirectory("directory").toFile(); 172 173 entries = new FileEntry[3]; 174 entries[0] = new FileEntry("foo.c", "foo.c", 0, 112); 175 entries[1] = new FileEntry("bar.h", "bar.h", Long.MAX_VALUE, 0); 176 // Will test getSimplifiedPath() behavior for ignored directories. 177 // Use DIRECTORY_INTERNAL_SIZE value for length so it is checked as the directory 178 // should contain "-" (DIRECTORY_SIZE_PLACEHOLDER) string. 179 entries[2] = new FileEntry("subdir", "subdir/", 0, Arrays.asList( 180 new FileEntry("SCCS", "SCCS/", 0, Arrays.asList( 181 new FileEntry("version", "version", 0, 312)) 182 ))); 183 184 for (FileEntry entry : entries) { 185 entry.create(); 186 } 187 188 // Create the entry that will be ignored separately. 189 FileEntry hgtags = new FileEntry(".hgtags", ".hgtags", 0, 1); 190 hgtags.create(); 191 192 // Need to populate list of ignored entries for all repository types. 193 RuntimeEnvironment env = RuntimeEnvironment.getInstance(); 194 RepositoryFactory.initializeIgnoredNames(env); 195 } 196 197 @After tearDown()198 public void tearDown() { 199 if (directory != null && directory.exists()) { 200 removeDirectory(directory); 201 directory.delete(); 202 } 203 } 204 removeDirectory(File dir)205 private void removeDirectory(File dir) { 206 File[] childs = dir.listFiles(); 207 if (childs != null) { 208 for (File f : childs) { 209 if (f.isDirectory()) { 210 removeDirectory(f); 211 } 212 f.delete(); 213 } 214 } 215 } 216 217 /** 218 * Get the href attribute from: <td align="left"><tt><a 219 * href="foo" class="p">foo</a></tt></td> 220 */ getHref(Node item)221 private String getHref(Node item) { 222 Node a = item.getFirstChild(); // a 223 assertNotNull(a); 224 assertEquals(Node.ELEMENT_NODE, a.getNodeType()); 225 226 Node href = a.getAttributes().getNamedItem("href"); 227 assertNotNull(href); 228 assertEquals(Node.ATTRIBUTE_NODE, href.getNodeType()); 229 230 return href.getNodeValue(); 231 } 232 233 /** 234 * Get the filename from: <td align="left"><tt><a href="foo" 235 * class="p">foo</a></tt></td> 236 */ getFilename(Node item)237 private String getFilename(Node item) { 238 Node a = item.getFirstChild(); // a 239 assertNotNull(a); 240 assertEquals(Node.ELEMENT_NODE, a.getNodeType()); 241 242 Node node = a.getFirstChild(); 243 assertNotNull(node); 244 // If this is element node then it is probably a directory in which case 245 // it contains the <b> element. 246 if (node.getNodeType() == Node.ELEMENT_NODE) { 247 node = node.getFirstChild(); 248 assertNotNull(node); 249 assertEquals(Node.TEXT_NODE, node.getNodeType()); 250 } else { 251 assertEquals(Node.TEXT_NODE, node.getNodeType()); 252 } 253 254 return node.getNodeValue(); 255 } 256 257 /** 258 * Get the LastModified date from the <td>date</td> 259 * 260 * @todo fix the item 261 * @param item the node representing <td> 262 * @return last modified date of the file 263 * @throws java.lang.Exception if an error occurs 264 */ getLastModified(Node item)265 private long getLastModified(Node item) throws Exception { 266 Node val = item.getFirstChild(); 267 assertNotNull(val); 268 assertEquals(Node.TEXT_NODE, val.getNodeType()); 269 270 String value = val.getNodeValue(); 271 return value.equalsIgnoreCase("Today") 272 ? Long.MAX_VALUE 273 : dateFormatter.parse(value).getTime(); 274 } 275 276 /** 277 * Get the size from the: <td><tt>size</tt></td> 278 * 279 * @param item the node representing <td> 280 * @return positive integer if the record was a file<br> 281 * -1 if the size could not be parsed<br> 282 * -2 if the record was a directory<br> 283 */ getSize(Node item)284 private int getSize(Node item) throws NumberFormatException { 285 Node val = item.getFirstChild(); 286 assertNotNull(val); 287 assertEquals(Node.TEXT_NODE, val.getNodeType()); 288 if (DirectoryListing.DIRECTORY_SIZE_PLACEHOLDER.equals(val.getNodeValue().trim())) { 289 // track that it had the DIRECTORY_SIZE_PLACEHOLDER character 290 return DIRECTORY_INTERNAL_SIZE; 291 } 292 try { 293 return Integer.parseInt(val.getNodeValue().trim()); 294 } catch (NumberFormatException ex) { 295 return INVALID_SIZE; 296 } 297 } 298 299 /** 300 * Validate this file-entry in the table 301 * 302 * @param element The <tr> element 303 * @throws java.lang.Exception 304 */ validateEntry(Element element)305 private void validateEntry(Element element) throws Exception { 306 FileEntry entry = new FileEntry(); 307 NodeList nl = element.getElementsByTagName("td"); 308 int len = nl.getLength(); 309 // There should be 5 columns or less in the table. 310 if (len < 5) { 311 return; 312 } 313 assertEquals("list.jsp table <td> count", 7, len); 314 315 // item(0) is a decoration placeholder, i.e. no content 316 entry.name = getFilename(nl.item(1)); 317 entry.href = getHref(nl.item(1)); 318 entry.lastModified = getLastModified(nl.item(3)); 319 entry.size = getSize(nl.item(4)); 320 321 // Try to look it up in the list of files. 322 for (int ii = 0; ii < entries.length; ++ii) { 323 if (entries[ii] != null && entries[ii].compareTo(entry) == 0) { 324 entries[ii] = null; 325 return; 326 } 327 } 328 329 fail("Could not find a match for: " + entry.name); 330 } 331 332 /** 333 * Test directory listing 334 * 335 * @throws java.lang.Exception if an error occurs while generating the list. 336 */ 337 @Test directoryListing()338 public void directoryListing() throws Exception { 339 StringWriter out = new StringWriter(); 340 out.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<start>\n"); 341 342 DirectoryListing instance = new DirectoryListing(); 343 instance.listTo("ctx", directory, out, directory.getPath(), 344 Arrays.asList(directory.list())); 345 346 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 347 assertNotNull("DocumentBuilderFactory is null", factory); 348 349 DocumentBuilder builder = factory.newDocumentBuilder(); 350 assertNotNull("DocumentBuilder is null", builder); 351 352 out.append("</start>\n"); 353 String str = out.toString(); 354 Document document = builder.parse(new ByteArrayInputStream(str.getBytes())); 355 356 NodeList nl = document.getElementsByTagName("tr"); 357 int len = nl.getLength(); 358 // Add one extra for header and one for parent directory link. 359 assertEquals(entries.length + 2, len); 360 // Skip the the header and parent link. 361 for (int i = 2; i < len; ++i) { 362 validateEntry((Element) nl.item(i)); 363 } 364 } 365 } 366