JAL-2119 case-sensitive href filename checking
[jalview.git] / utils / HelpLinksChecker.java
1
2
3 import java.io.BufferedReader;
4 import java.io.File;
5 import java.io.FileReader;
6 import java.io.IOException;
7 import java.io.InputStream;
8 import java.net.URL;
9 import java.util.HashMap;
10 import java.util.Map;
11
12 /**
13  * A class to check help file cross-references, and external URLs if internet
14  * access is available
15  * 
16  * @author gmcarstairs
17  *
18  */
19 public class HelpLinksChecker
20 {
21   private static final String HELP_HS = "help.hs";
22
23   private static final String HELP_TOC_XML = "helpTOC.xml";
24
25   private static final String HELP_JHM = "help.jhm";
26
27   private static boolean internetAvailable = true;
28
29   private int targetCount = 0;
30
31   private int mapCount = 0;
32
33   private int internalHrefCount = 0;
34
35   private int anchorRefCount = 0;
36
37   private int invalidAnchorRefCount = 0;
38
39   private int externalHrefCount = 0;
40
41   private int invalidMapUrlCount = 0;
42
43   private int invalidTargetCount = 0;
44
45   private int invalidImageCount = 0;
46
47   private int invalidInternalHrefCount = 0;
48
49   private int invalidExternalHrefCount = 0;
50
51   /**
52    * The only parameter should be a path to the root of the help directory in
53    * the workspace
54    * 
55    * @param args
56    *          [0] path to the /html folder in the workspace
57    * @param args
58    *          [1] (optional) -nointernet to suppress external link checking for
59    *          a fast check of internal links only
60    * @throws IOException
61    */
62   public static void main(String[] args) throws IOException
63   {
64     if (args.length == 0 || args.length > 2
65             || (args.length == 2 && !args[1].equals("-nointernet")))
66     {
67       log("Usage: <pathToHelpFolder> [-nointernet]");
68       return;
69     }
70
71     if (args.length == 2)
72     {
73       internetAvailable = false;
74     }
75
76     new HelpLinksChecker().checkLinks(args[0]);
77   }
78
79   /**
80    * Checks help links and reports results
81    * 
82    * @param helpDirectoryPath
83    * @throws IOException
84    */
85   void checkLinks(String helpDirectoryPath) throws IOException
86   {
87     log("Checking help file links");
88     File helpFolder = new File(helpDirectoryPath).getCanonicalFile();
89     if (!helpFolder.exists())
90     {
91       log("Can't find " + helpDirectoryPath);
92       return;
93     }
94
95     internetAvailable &= connectToUrl("http://www.example.org");
96
97     Map<String, String> tocTargets = checkHelpMappings(helpFolder);
98
99     Map<String, String> unusedTargets = new HashMap<String, String>(
100             tocTargets);
101
102     checkTableOfContents(helpFolder, tocTargets, unusedTargets);
103
104     checkHelpSet(helpFolder, tocTargets, unusedTargets);
105
106     checkHtmlFolder(new File(helpFolder, "html"));
107
108     reportResults(unusedTargets);
109   }
110
111   /**
112    * Checks all html files in the given directory or its sub-directories
113    * 
114    * @param folder
115    * @throws IOException
116    */
117   private void checkHtmlFolder(File folder) throws IOException
118   {
119     File[] files = folder.listFiles();
120     for (File f : files)
121     {
122       if (f.isDirectory())
123       {
124         checkHtmlFolder(f);
125       }
126       else
127       {
128         if (f.getAbsolutePath().endsWith(".html"))
129         {
130           checkHtmlFile(f, folder);
131         }
132       }
133     }
134   }
135
136   /**
137    * Checks that any image attribute in help.hs is a valid target
138    * 
139    * @param helpFolder
140    * @param tocTargets
141    * @param unusedTargets
142    *          used targets are removed from here
143    */
144   private void checkHelpSet(File helpFolder,
145           Map<String, String> tocTargets, Map<String, String> unusedTargets)
146           throws IOException
147   {
148     BufferedReader br = new BufferedReader(new FileReader(new File(
149             helpFolder, HELP_HS)));
150     String data = br.readLine();
151     int lineNo = 0;
152
153     while (data != null)
154     {
155       lineNo++;
156       String image = getAttribute(data, "image");
157       if (image != null)
158       {
159         unusedTargets.remove(image);
160         if (!tocTargets.containsKey(image))
161         {
162           log(String.format(
163                   "Invalid image '%s' at line %d of %s", image, lineNo,
164                   HELP_HS));
165           invalidImageCount++;
166         }
167       }
168       data = br.readLine();
169     }
170     br.close();
171   }
172
173   /**
174    * Print counts to sysout
175    * 
176    * @param unusedTargets
177    */
178   private void reportResults(Map<String, String> unusedTargets)
179   {
180     log("\nResults:");
181     log(targetCount + " distinct help targets");
182     log(mapCount + " help mappings");
183     log(invalidTargetCount + " invalid targets");
184     log(unusedTargets.size() + " unused targets");
185     for (String target : unusedTargets.keySet())
186     {
187       log(String.format("    %s: %s", target,
188               unusedTargets.get(target)));
189     }
190     log(invalidMapUrlCount + " invalid map urls");
191     log(invalidImageCount + " invalid image attributes");
192     log(String.format(
193             "%d internal href links (%d with anchors)", internalHrefCount,
194             anchorRefCount));
195     log(invalidInternalHrefCount
196             + " invalid internal href links");
197     log(invalidAnchorRefCount
198             + " invalid internal anchor links");
199     log(externalHrefCount + " external href links");
200     if (internetAvailable)
201     {
202       log(invalidExternalHrefCount
203               + " invalid external href links");
204     }
205     else
206     {
207       System.out
208               .println("External links not verified as internet not available");
209     }
210     if (invalidInternalHrefCount > 0 || invalidExternalHrefCount > 0
211             || invalidImageCount > 0 || invalidAnchorRefCount > 0)
212     {
213       log("*** Failed ***");
214       System.exit(1);
215     }
216     log("*** Success ***");
217   }
218
219   /**
220    * @param s
221    */
222   static void log(String s)
223   {
224     System.out.println(s);
225   }
226
227   /**
228    * Reads the given html file and checks any href attibute values are either
229    * <ul>
230    * <li>a valid relative file path, or</li>
231    * <li>a valid absolute URL (if external link checking is enabled)</li>
232    * </ul>
233    * 
234    * @param htmlFile
235    * @param htmlFolder
236    *          the parent folder (for validation of relative paths)
237    */
238   private void checkHtmlFile(File htmlFile, File htmlFolder)
239           throws IOException
240   {
241     BufferedReader br = new BufferedReader(new FileReader(htmlFile));
242     String data = br.readLine();
243     int lineNo = 0;
244     while (data != null)
245     {
246       lineNo++;
247       String href = getAttribute(data, "href");
248       if (href != null)
249       {
250         String anchor = null;
251         int anchorPos = href.indexOf("#");
252         if (anchorPos != -1)
253         {
254           anchor = href.substring(anchorPos + 1);
255           href = href.substring(0, anchorPos);
256         }
257         boolean badLink = false;
258         if (href.startsWith("http"))
259         {
260           externalHrefCount++;
261           if (internetAvailable)
262           {
263             if (!connectToUrl(href))
264             {
265               badLink = true;
266               invalidExternalHrefCount++;
267             }
268           }
269         }
270         else
271         {
272           internalHrefCount++;
273           File hrefFile = href.equals("") ? htmlFile : new File(htmlFolder,
274                   href);
275           if (hrefFile != htmlFile && !fileExists(hrefFile, href))
276           {
277             badLink = true;
278             invalidInternalHrefCount++;
279           }
280           if (anchor != null)
281           {
282             anchorRefCount++;
283             if (!badLink)
284             {
285               if (!checkAnchorExists(hrefFile, anchor))
286               {
287                 log(String.format(
288                         "Invalid anchor: %s at line %d of %s", anchor,
289                         lineNo, getPath(htmlFile)));
290                 invalidAnchorRefCount++;
291               }
292             }
293           }
294         }
295         if (badLink)
296         {
297           log(String.format(
298                   "Invalid href %s at line %d of %s", href, lineNo,
299                   getPath(htmlFile)));
300         }
301       }
302       data = br.readLine();
303     }
304     br.close();
305   }
306
307   /**
308    * Performs a case-sensitive check that the href'd file exists
309    * 
310    * @param hrefFile
311    * @return
312    * @throws IOException
313    */
314   boolean fileExists(File hrefFile, String href) throws IOException
315   {
316     if (!hrefFile.exists())
317     {
318       return false;
319     }
320
321     /*
322      * On Mac or Windows, file.exists() is not case sensitive, so do an
323      * additional check with case sensitivity 
324      */
325     int slashPos = href.lastIndexOf("/");
326     String expectedFileName = slashPos == -1 ? href : href
327             .substring(slashPos + 1);
328     String cp = hrefFile.getCanonicalPath();
329     slashPos = cp.lastIndexOf("/");
330     String actualFileName = slashPos == -1 ? cp : cp
331             .substring(slashPos + 1);
332
333     return expectedFileName.equals(actualFileName);
334   }
335
336   /**
337    * Reads the file and checks for the presence of the given html anchor
338    * 
339    * @param hrefFile
340    * @param anchor
341    * @return true if anchor is found else false
342    */
343   private boolean checkAnchorExists(File hrefFile, String anchor)
344   {
345     String nameAnchor = "<a name=\"" + anchor + "\"";
346     String idAnchor = "<a id=\"" + anchor + "\"";
347     boolean found = false;
348     try
349     {
350       BufferedReader br = new BufferedReader(new FileReader(hrefFile));
351       String data = br.readLine();
352       while (data != null)
353       {
354         if (data.contains(nameAnchor) || data.contains(idAnchor))
355         {
356           found = true;
357           break;
358         }
359         data = br.readLine();
360       }
361       br.close();
362     } catch (IOException e)
363     {
364       // ignore
365     }
366     return found;
367   }
368
369   /**
370    * Returns the part of the file path starting from /help/
371    * 
372    * @param helpFile
373    * @return
374    */
375   private String getPath(File helpFile)
376   {
377     String path = helpFile.getPath();
378     int helpPos = path.indexOf("/help/");
379     return helpPos == -1 ? path : path.substring(helpPos);
380   }
381
382   /**
383    * Returns true if the URL returns an input stream, or false if the URL
384    * returns an error code or we cannot connect to it (e.g. no internet
385    * available)
386    * 
387    * @param url
388    * @return
389    */
390   private boolean connectToUrl(String url)
391   {
392     try
393     {
394       URL u = new URL(url);
395       InputStream connection = u.openStream();
396       connection.close();
397       return true;
398     } catch (Throwable t)
399     {
400       return false;
401     }
402   }
403
404   /**
405    * Reads file help.jhm and checks that
406    * <ul>
407    * <li>each target attribute is in tocTargets</li>
408    * <li>each url attribute is a valid relative file link</li>
409    * </ul>
410    * 
411    * @param helpFolder
412    */
413   private Map<String, String> checkHelpMappings(File helpFolder)
414           throws IOException
415   {
416     Map<String, String> targets = new HashMap<String, String>();
417     BufferedReader br = new BufferedReader(new FileReader(new File(
418             helpFolder, HELP_JHM)));
419     String data = br.readLine();
420     int lineNo = 0;
421     while (data != null)
422     {
423       lineNo++;
424
425       /*
426        * record target, check for duplicates
427        */
428       String target = getAttribute(data, "target");
429       if (target != null)
430       {
431         mapCount++;
432         if (targets.containsKey(target))
433         {
434           log(String.format(
435                   "Duplicate target mapping to %s at line %d of %s",
436                   target, lineNo, HELP_JHM));
437         }
438         else
439         {
440           targetCount++;
441         }
442       }
443
444       /*
445        * validate url
446        */
447       String url = getAttribute(data, "url");
448       if (url != null)
449       {
450         targets.put(target, url);
451         int anchorPos = url.indexOf("#");
452         if (anchorPos != -1)
453         {
454           url = url.substring(0, anchorPos);
455         }
456         if (!new File(helpFolder, url).exists())
457         {
458           log(String.format(
459                   "Invalid url path '%s' at line %d of %s", url, lineNo,
460                   HELP_JHM));
461           invalidMapUrlCount++;
462         }
463       }
464       data = br.readLine();
465     }
466     br.close();
467     return targets;
468   }
469
470   /**
471    * Reads file helpTOC.xml and reports any invalid targets
472    * 
473    * @param helpFolder
474    * @param tocTargets
475    * @param unusedTargets
476    *          used targets are removed from this map
477    * 
478    * @return
479    * @throws IOException
480    */
481   private void checkTableOfContents(File helpFolder,
482           Map<String, String> tocTargets, Map<String, String> unusedTargets)
483           throws IOException
484   {
485     BufferedReader br = new BufferedReader(new FileReader(new File(
486             helpFolder, HELP_TOC_XML)));
487     String data = br.readLine();
488     int lineNo = 0;
489     while (data != null)
490     {
491       lineNo++;
492       /*
493        * assuming no more than one "target" per line of file here
494        */
495       String target = getAttribute(data, "target");
496       if (target != null)
497       {
498         unusedTargets.remove(target);
499         if (!tocTargets.containsKey(target))
500         {
501           log(String.format(
502                   "Invalid target '%s' at line %d of %s", target, lineNo,
503                   HELP_TOC_XML));
504           invalidTargetCount++;
505         }
506       }
507       data = br.readLine();
508     }
509     br.close();
510   }
511
512   /**
513    * Returns the value of an attribute if found in the data, else null
514    * 
515    * @param data
516    * @param attName
517    * @return
518    */
519   private static String getAttribute(String data, String attName)
520   {
521     /*
522      * make a partial attempt at ignoring within <!-- html comments -->
523      * (doesn't work if multi-line)
524      */
525     int commentStartPos = data.indexOf("<!--");
526     int commentEndPos = commentStartPos == -1 ? -1 : data.substring(
527             commentStartPos + 4).indexOf("-->");
528     String value = null;
529     String match = attName + "=\"";
530     int attPos = data.indexOf(match);
531     if (attPos > 0
532             && (commentStartPos == -1 || attPos < commentStartPos || attPos > commentEndPos))
533     {
534       data = data.substring(attPos + match.length());
535       value = data.substring(0, data.indexOf("\""));
536     }
537     return value;
538   }
539 }