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: &lt;td align="left"&gt;&lt;tt&gt;&lt;a
219      * href="foo" class="p"&gt;foo&lt;/a&gt;&lt;/tt&gt;&lt;/td&gt;
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: &lt;td align="left"&gt;&lt;tt&gt;&lt;a href="foo"
235      * class="p"&gt;foo&lt;/a&gt;&lt;/tt&gt;&lt;/td&gt;
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 &lt;b&gt; 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 &lt;td&gt;date&lt;/td&gt;
259      *
260      * @todo fix the item
261      * @param item the node representing &lt;td&gt
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: &lt;td&gt;&lt;tt&gt;size&lt;/tt&gt;&lt;/td&gt;
278      *
279      * @param item the node representing &lt;td&gt;
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 &lt;tr&gt; 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