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