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