/* * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$) * Copyright (C) $$Year-Rel$$ The Jalview Authors * * This file is part of Jalview. * * Jalview is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * Jalview is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty * of MERCHANTABILITY or FITNESS FOR A PARTICULAR * PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Jalview. If not, see . * The Jalview Authors are detailed in the 'AUTHORS' file. */ import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.HashMap; import java.util.Map; /** * A class to check help file cross-references, and external URLs if internet * access is available * * @author gmcarstairs * */ public class HelpLinksChecker implements BufferedLineReader.LineCleaner { private static final String HELP_HS = "help.hs"; private static final String HELP_TOC_XML = "helpTOC.xml"; private static final String HELP_JHM = "help.jhm"; private static boolean internetAvailable = true; private int targetCount = 0; private int mapCount = 0; private int internalHrefCount = 0; private int anchorRefCount = 0; private int invalidAnchorRefCount = 0; private int externalHrefCount = 0; private int invalidMapUrlCount = 0; private int invalidTargetCount = 0; private int invalidImageCount = 0; private int invalidInternalHrefCount = 0; private int invalidExternalHrefCount = 0; /** * The only parameter should be a path to the root of the help directory in * the workspace * * @param args * [0] path to the /html folder in the workspace * @param args * [1] (optional) -nointernet to suppress external link checking for * a fast check of internal links only * @throws IOException */ public static void main(String[] args) throws IOException { if (args.length == 0 || args.length > 2 || (args.length == 2 && !args[1].equals("-nointernet"))) { log("Usage: [-nointernet]"); return; } if (args.length == 2) { internetAvailable = false; } new HelpLinksChecker().checkLinks(args[0]); } /** * Checks help links and reports results * * @param helpDirectoryPath * @throws IOException */ void checkLinks(String helpDirectoryPath) throws IOException { log("Checking help file links"); File helpFolder = new File(helpDirectoryPath).getCanonicalFile(); if (!helpFolder.exists()) { log("Can't find " + helpDirectoryPath); return; } internetAvailable &= connectToUrl("http://www.example.org"); Map tocTargets = checkHelpMappings(helpFolder); Map unusedTargets = new HashMap( tocTargets); checkTableOfContents(helpFolder, tocTargets, unusedTargets); checkHelpSet(helpFolder, tocTargets, unusedTargets); checkHtmlFolder(new File(helpFolder, "html")); reportResults(unusedTargets); } /** * Checks all html files in the given directory or its sub-directories * * @param folder * @throws IOException */ private void checkHtmlFolder(File folder) throws IOException { File[] files = folder.listFiles(); for (File f : files) { if (f.isDirectory()) { checkHtmlFolder(f); } else { if (f.getAbsolutePath().endsWith(".html")) { checkHtmlFile(f, folder); } } } } /** * Checks that any image attribute in help.hs is a valid target * * @param helpFolder * @param tocTargets * @param unusedTargets * used targets are removed from here */ private void checkHelpSet(File helpFolder, Map tocTargets, Map unusedTargets) throws IOException { BufferedReader br = new BufferedReader(new FileReader(new File( helpFolder, HELP_HS))); String data = br.readLine(); int lineNo = 0; while (data != null) { lineNo++; String image = getAttribute(data, "image"); if (image != null) { unusedTargets.remove(image); if (!tocTargets.containsKey(image)) { log(String.format("Invalid image '%s' at line %d of %s", image, lineNo, HELP_HS)); invalidImageCount++; } } data = br.readLine(); } br.close(); } /** * Print counts to sysout * * @param unusedTargets */ private void reportResults(Map unusedTargets) { log("\nResults:"); log(targetCount + " distinct help targets"); log(mapCount + " help mappings"); log(invalidTargetCount + " invalid targets"); log(unusedTargets.size() + " unused targets"); for (String target : unusedTargets.keySet()) { log(String.format(" %s: %s", target, unusedTargets.get(target))); } log(invalidMapUrlCount + " invalid map urls"); log(invalidImageCount + " invalid image attributes"); log(String.format("%d internal href links (%d with anchors)", internalHrefCount, anchorRefCount)); log(invalidInternalHrefCount + " invalid internal href links"); log(invalidAnchorRefCount + " invalid internal anchor links"); log(externalHrefCount + " external href links"); if (internetAvailable) { log(invalidExternalHrefCount + " invalid external href links"); } else { System.out .println("External links not verified as internet not available"); } if (invalidInternalHrefCount > 0 || invalidExternalHrefCount > 0 || invalidImageCount > 0 || invalidAnchorRefCount > 0) { log("*** Failed ***"); System.exit(1); } log("*** Success ***"); } /** * @param s */ static void log(String s) { System.out.println(s); } /** * Reads the given html file and checks any href attibute values are either *
    *
  • a valid relative file path, or
  • *
  • a valid absolute URL (if external link checking is enabled)
  • *
* * @param htmlFile * @param htmlFolder * the parent folder (for validation of relative paths) */ private void checkHtmlFile(File htmlFile, File htmlFolder) throws IOException { BufferedReader br = new BufferedReader(new FileReader(htmlFile)); String data = br.readLine(); int lineNo = 0; while (data != null) { lineNo++; String href = getAttribute(data, "href"); if (href != null) { String anchor = null; int anchorPos = href.indexOf("#"); if (anchorPos != -1) { anchor = href.substring(anchorPos + 1); href = href.substring(0, anchorPos); } boolean badLink = false; if (href.startsWith("http")) { externalHrefCount++; if (internetAvailable) { if (!connectToUrl(href)) { badLink = true; invalidExternalHrefCount++; } } } else { internalHrefCount++; String relFile = System.getProperty("os.name").indexOf("Win") > -1 ? href.replace("/", File.separator) : href; File hrefFile = href.equals("") ? htmlFile : new File(htmlFolder, href); if (hrefFile != htmlFile && !fileExists(hrefFile, relFile)) { badLink = true; invalidInternalHrefCount++; } if (anchor != null) { anchorRefCount++; if (!badLink) { if (!checkAnchorExists(hrefFile, anchor)) { log(String.format("Invalid anchor: %s at line %d of %s", anchor, lineNo, getPath(htmlFile))); invalidAnchorRefCount++; } } } } if (badLink) { log(String.format("Invalid href %s at line %d of %s", href, lineNo, getPath(htmlFile))); } } data = br.readLine(); } br.close(); } /** * Performs a case-sensitive check that the href'd file exists * * @param hrefFile * @return * @throws IOException */ boolean fileExists(File hrefFile, String href) throws IOException { if (!hrefFile.exists()) { return false; } /* * On Mac or Windows, file.exists() is not case sensitive, so do an * additional check with case sensitivity */ int slashPos = href.lastIndexOf(File.separator); String expectedFileName = slashPos == -1 ? href : href .substring(slashPos + 1); String cp = hrefFile.getCanonicalPath(); slashPos = cp.lastIndexOf(File.separator); String actualFileName = slashPos == -1 ? cp : cp .substring(slashPos + 1); return expectedFileName.equals(actualFileName); } /** * Reads the file and checks for the presence of the given html anchor * * @param hrefFile * @param anchor * @return true if anchor is found else false */ private boolean checkAnchorExists(File hrefFile, String anchor) { String nameAnchor = " *
  • each target attribute is in tocTargets
  • *
  • each url attribute is a valid relative file link
  • * * * @param helpFolder */ private Map checkHelpMappings(File helpFolder) throws IOException { Map targets = new HashMap(); BufferedReader br = new BufferedReader(new FileReader(new File( helpFolder, HELP_JHM))); String data = br.readLine(); int lineNo = 0; while (data != null) { lineNo++; /* * record target, check for duplicates */ String target = getAttribute(data, "target"); if (target != null) { mapCount++; if (targets.containsKey(target)) { log(String.format( "Duplicate target mapping to %s at line %d of %s", target, lineNo, HELP_JHM)); } else { targetCount++; } } /* * validate url */ String url = getAttribute(data, "url"); if (url != null) { targets.put(target, url); int anchorPos = url.indexOf("#"); if (anchorPos != -1) { url = url.substring(0, anchorPos); } if (!new File(helpFolder, url).exists()) { log(String.format("Invalid url path '%s' at line %d of %s", url, lineNo, HELP_JHM)); invalidMapUrlCount++; } } data = br.readLine(); } br.close(); return targets; } /** * Reads file helpTOC.xml and reports any invalid targets * * @param helpFolder * @param tocTargets * @param unusedTargets * used targets are removed from this map * * @return * @throws IOException */ private void checkTableOfContents(File helpFolder, Map tocTargets, Map unusedTargets) throws IOException { BufferedReader br = new BufferedReader(new FileReader(new File( helpFolder, HELP_TOC_XML))); String data = br.readLine(); int lineNo = 0; while (data != null) { lineNo++; /* * assuming no more than one "target" per line of file here */ String target = getAttribute(data, "target"); if (target != null) { unusedTargets.remove(target); if (!tocTargets.containsKey(target)) { log(String.format("Invalid target '%s' at line %d of %s", target, lineNo, HELP_TOC_XML)); invalidTargetCount++; } } data = br.readLine(); } br.close(); } /** * Returns the value of an attribute if found in the data, else null * * @param data * @param attName * @return */ private static String getAttribute(String data, String attName) { /* * make a partial attempt at ignoring within * (doesn't work if multi-line) */ int commentStartPos = data.indexOf(""); String value = null; String match = attName + "=\""; int attPos = data.indexOf(match); if (attPos > 0 && (commentStartPos == -1 || attPos < commentStartPos || attPos > commentEndPos)) { data = data.substring(attPos + match.length()); value = data.substring(0, data.indexOf("\"")); } return value; } /** * Trim whitespace from concatenated lines but preserve one space for valid * parsing */ @Override public String cleanLine(String l) { return l.trim() + " "; } }