JAL-2189 formatting and GPL
[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
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       String data = br.readLine();
362       while (data != null)
363       {
364         if (data.contains(nameAnchor) || data.contains(idAnchor))
365         {
366           found = true;
367           break;
368         }
369         data = br.readLine();
370       }
371       br.close();
372     } catch (IOException e)
373     {
374       // ignore
375     }
376     return found;
377   }
378
379   /**
380    * Returns the part of the file path starting from /help/
381    * 
382    * @param helpFile
383    * @return
384    */
385   private String getPath(File helpFile)
386   {
387     String path = helpFile.getPath();
388     int helpPos = path.indexOf("/help/");
389     return helpPos == -1 ? path : path.substring(helpPos);
390   }
391
392   /**
393    * Returns true if the URL returns an input stream, or false if the URL
394    * returns an error code or we cannot connect to it (e.g. no internet
395    * available)
396    * 
397    * @param url
398    * @return
399    */
400   private boolean connectToUrl(String url)
401   {
402     try
403     {
404       URL u = new URL(url);
405       InputStream connection = u.openStream();
406       connection.close();
407       return true;
408     } catch (Throwable t)
409     {
410       return false;
411     }
412   }
413
414   /**
415    * Reads file help.jhm and checks that
416    * <ul>
417    * <li>each target attribute is in tocTargets</li>
418    * <li>each url attribute is a valid relative file link</li>
419    * </ul>
420    * 
421    * @param helpFolder
422    */
423   private Map<String, String> checkHelpMappings(File helpFolder)
424           throws IOException
425   {
426     Map<String, String> targets = new HashMap<String, String>();
427     BufferedReader br = new BufferedReader(new FileReader(new File(
428             helpFolder, HELP_JHM)));
429     String data = br.readLine();
430     int lineNo = 0;
431     while (data != null)
432     {
433       lineNo++;
434
435       /*
436        * record target, check for duplicates
437        */
438       String target = getAttribute(data, "target");
439       if (target != null)
440       {
441         mapCount++;
442         if (targets.containsKey(target))
443         {
444           log(String.format(
445                   "Duplicate target mapping to %s at line %d of %s",
446                   target, lineNo, HELP_JHM));
447         }
448         else
449         {
450           targetCount++;
451         }
452       }
453
454       /*
455        * validate url
456        */
457       String url = getAttribute(data, "url");
458       if (url != null)
459       {
460         targets.put(target, url);
461         int anchorPos = url.indexOf("#");
462         if (anchorPos != -1)
463         {
464           url = url.substring(0, anchorPos);
465         }
466         if (!new File(helpFolder, url).exists())
467         {
468           log(String.format("Invalid url path '%s' at line %d of %s", url,
469                   lineNo, HELP_JHM));
470           invalidMapUrlCount++;
471         }
472       }
473       data = br.readLine();
474     }
475     br.close();
476     return targets;
477   }
478
479   /**
480    * Reads file helpTOC.xml and reports any invalid targets
481    * 
482    * @param helpFolder
483    * @param tocTargets
484    * @param unusedTargets
485    *          used targets are removed from this map
486    * 
487    * @return
488    * @throws IOException
489    */
490   private void checkTableOfContents(File helpFolder,
491           Map<String, String> tocTargets, Map<String, String> unusedTargets)
492           throws IOException
493   {
494     BufferedReader br = new BufferedReader(new FileReader(new File(
495             helpFolder, HELP_TOC_XML)));
496     String data = br.readLine();
497     int lineNo = 0;
498     while (data != null)
499     {
500       lineNo++;
501       /*
502        * assuming no more than one "target" per line of file here
503        */
504       String target = getAttribute(data, "target");
505       if (target != null)
506       {
507         unusedTargets.remove(target);
508         if (!tocTargets.containsKey(target))
509         {
510           log(String.format("Invalid target '%s' at line %d of %s", target,
511                   lineNo, HELP_TOC_XML));
512           invalidTargetCount++;
513         }
514       }
515       data = br.readLine();
516     }
517     br.close();
518   }
519
520   /**
521    * Returns the value of an attribute if found in the data, else null
522    * 
523    * @param data
524    * @param attName
525    * @return
526    */
527   private static String getAttribute(String data, String attName)
528   {
529     /*
530      * make a partial attempt at ignoring within <!-- html comments -->
531      * (doesn't work if multi-line)
532      */
533     int commentStartPos = data.indexOf("<!--");
534     int commentEndPos = commentStartPos == -1 ? -1 : data.substring(
535             commentStartPos + 4).indexOf("-->");
536     String value = null;
537     String match = attName + "=\"";
538     int attPos = data.indexOf(match);
539     if (attPos > 0
540             && (commentStartPos == -1 || attPos < commentStartPos || attPos > commentEndPos))
541     {
542       data = data.substring(attPos + match.length());
543       value = data.substring(0, data.indexOf("\""));
544     }
545     return value;
546   }
547 }