+ */
+public abstract class Checker {
+ private SingleCheckResults checkingResults;
+ private Configuration myConfig;
+
+ public Checker(Configuration pConfig) {
+ this.myConfig = pConfig;
+ }
+
+ /**
+ * * template method for performing a single type of checks on the given @see HtmlPage.
+ *
+ * Prerequisite: pageToCheck has been successfully parsed,
+ * prior to constructing this Checker instance.
+ **/
+ public SingleCheckResults performCheck(final HtmlPage pageToCheck) {
+ // assert non-null htmlPage
+ assert pageToCheck != null;
+
+ checkingResults = new SingleCheckResults();
+
+ // description is set by subclasses
+ initCheckingResultsDescription();
+
+ return check(pageToCheck);// <1> delegate check() to subclass
+ }
+
+ /**
+ * Initialize with suitable description.
+ */
+ protected abstract void initCheckingResultsDescription();
+
+ /**
+ * Perform a particular kind of checks, i.e. missing-local-images-check
+ *
+ * Called by {@link #performCheck()} as part of the template method pattern.
+ *
+ * @return collected results of this Checker instance
+ */
+ protected abstract SingleCheckResults check(final HtmlPage pageToCheck);
+
+ public SingleCheckResults getCheckingResults() {
+ return checkingResults;
+ }
+
+ public void setCheckingResults(SingleCheckResults checkingResults) {
+ this.checkingResults = checkingResults;
+ }
+
+ public Configuration getMyConfig() {
+ return myConfig;
+ }
+
+ public void setMyConfig(Configuration myConfig) {
+ this.myConfig = myConfig;
+ }
+}
diff --git a/htmlSanityCheck-core/src/main/java/org/aim42/htmlsanitycheck/check/CheckerCreator.java b/htmlSanityCheck-core/src/main/java/org/aim42/htmlsanitycheck/check/CheckerCreator.java
new file mode 100644
index 00000000..2fa8b9d6
--- /dev/null
+++ b/htmlSanityCheck-core/src/main/java/org/aim42/htmlsanitycheck/check/CheckerCreator.java
@@ -0,0 +1,62 @@
+package org.aim42.htmlsanitycheck.check;
+
+import org.aim42.htmlsanitycheck.Configuration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * abstract factory to create Checker instances
+ */
+public class CheckerCreator {
+ private static final Logger logger = LoggerFactory.getLogger(CheckerCreator.class);
+
+ public static Set createCheckerClassesFrom(final Collection> checkerClasses, final Configuration pConfig) {
+
+ return checkerClasses.stream()
+ .map(checkerClass -> CheckerCreator.createSingleChecker(checkerClass, pConfig))
+ .collect(Collectors.toSet());
+ }
+
+ private static boolean isCase(Class extends Checker> caseValue, Class extends Checker> switchValue) {
+ if (switchValue != null) {
+ return caseValue.isAssignableFrom(switchValue);
+ }
+ return false;
+ }
+
+ public static Checker createSingleChecker(final Class extends Checker> checkerClass, final Configuration pConfig) {
+ Checker checker;
+
+ // switch over all possible Checker classes
+ // in case of new Checkers, this has to be adapted,
+ // as Checker constructors will differ in minor details!
+
+ // clearly violates the open-close principle
+
+ if (isCase(BrokenCrossReferencesChecker.class, checkerClass)) {
+ checker = new BrokenCrossReferencesChecker(pConfig);
+ } else if (isCase(BrokenHttpLinksChecker.class, checkerClass)) {
+ checker = new BrokenHttpLinksChecker(pConfig);
+ } else if (isCase(DuplicateIdChecker.class, checkerClass)) {
+ checker = new DuplicateIdChecker(pConfig);
+ } else if (isCase(ImageMapChecker.class, checkerClass)) {
+ checker = new ImageMapChecker(pConfig);
+ } else if (isCase(MissingAltInImageTagsChecker.class, checkerClass)) {
+ checker = new MissingAltInImageTagsChecker(pConfig);
+ } else if (isCase(MissingImageFilesChecker.class, checkerClass)) {
+ checker = new MissingImageFilesChecker(pConfig);
+ } else if (isCase(MissingLocalResourcesChecker.class, checkerClass)) {
+ checker = new MissingLocalResourcesChecker(pConfig);
+ } else {
+ logger.warn("unknown Checker " + checkerClass.toString());
+ throw new UnknownCheckerException(checkerClass.toString());
+ }
+
+ return checker;
+
+ }
+}
diff --git a/htmlSanityCheck-core/src/main/java/org/aim42/htmlsanitycheck/check/DuplicateIdChecker.java b/htmlSanityCheck-core/src/main/java/org/aim42/htmlsanitycheck/check/DuplicateIdChecker.java
new file mode 100644
index 00000000..d1b902c6
--- /dev/null
+++ b/htmlSanityCheck-core/src/main/java/org/aim42/htmlsanitycheck/check/DuplicateIdChecker.java
@@ -0,0 +1,84 @@
+package org.aim42.htmlsanitycheck.check;
+
+import org.aim42.htmlsanitycheck.Configuration;
+import org.aim42.htmlsanitycheck.collect.SingleCheckResults;
+import org.aim42.htmlsanitycheck.html.HtmlElement;
+import org.aim42.htmlsanitycheck.html.HtmlPage;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class DuplicateIdChecker extends Checker {
+ private Set idStringsSet;
+ private List idStringsList;
+
+ public DuplicateIdChecker(Configuration pConfig) {
+ super(pConfig);
+ }
+
+ /**
+ * find all tags with specific id value
+ *
+ * @param id
+ * @param allTags List of tags containing id-attribute
+ */
+ public static List getAllTagsWithSpecificId(final String idString, List allTags) {
+ return allTags.stream().filter(htmlElement -> htmlElement.getIdAttribute().equals(idString)).collect(Collectors.toList());
+
+ }
+
+ @Override
+ protected void initCheckingResultsDescription() {
+ getCheckingResults().setWhatIsChecked("Duplicate Definition of id Check");
+ getCheckingResults().setSourceItemName("id");
+ getCheckingResults().setTargetItemName("duplicate id");
+ }
+
+ @Override
+ protected SingleCheckResults check(final HtmlPage pageToCheck) {
+
+ //get list of all tagsWithId '<... id="XYZ"...' in html file
+
+ idStringsList = pageToCheck.getAllIdStrings();
+ idStringsSet = new HashSet<>(idStringsList);
+
+ checkForDuplicateIds(idStringsSet);
+
+ return getCheckingResults();
+
+ }
+
+ private void checkForDuplicateIds(Set idStringsSet) {
+ idStringsSet.forEach(oneIdString -> checkForDuplicateDefinition(oneIdString));
+ }
+
+ private void checkForDuplicateDefinition(final String idString) {
+ getCheckingResults().incNrOfChecks();
+
+ int nrOfOccurrences = (int) idStringsList.stream().filter(it -> it.equals(idString)).count();
+
+ // duplicate, IFF idString appears more than once in idStringsList
+ if (nrOfOccurrences > 1) {
+ getCheckingResults().newFinding("id \"" + idString + "\" has " + nrOfOccurrences + " definitions.");
+ }
+
+ }
+
+ public Set getIdStringsSet() {
+ return idStringsSet;
+ }
+
+ public void setIdStringsSet(Set idStringsSet) {
+ this.idStringsSet = idStringsSet;
+ }
+
+ public List getIdStringsList() {
+ return idStringsList;
+ }
+
+ public void setIdStringsList(List idStringsList) {
+ this.idStringsList = idStringsList;
+ }
+}
diff --git a/htmlSanityCheck-core/src/main/java/org/aim42/htmlsanitycheck/check/ImageMapChecker.java b/htmlSanityCheck-core/src/main/java/org/aim42/htmlsanitycheck/check/ImageMapChecker.java
new file mode 100644
index 00000000..3de60b03
--- /dev/null
+++ b/htmlSanityCheck-core/src/main/java/org/aim42/htmlsanitycheck/check/ImageMapChecker.java
@@ -0,0 +1,186 @@
+package org.aim42.htmlsanitycheck.check;
+
+import org.aim42.htmlsanitycheck.Configuration;
+import org.aim42.htmlsanitycheck.collect.Finding;
+import org.aim42.htmlsanitycheck.collect.SingleCheckResults;
+import org.aim42.htmlsanitycheck.html.HtmlElement;
+import org.aim42.htmlsanitycheck.html.HtmlPage;
+import org.aim42.htmlsanitycheck.tools.Web;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * principal checks on imageMap usage:
+ *
+ * 1.) for every usemap-reference there is one map
+ * 2.) every map is referenced by at least one image
+ * 3.) every every map name is unique
+ * 4.) every area-tag has one non-empty href attribute
+ * 5.) every href points to valid target (broken-links check)
+ *
+ * see also: http://www.w3schools.com/tags/tag_map.asp
+ **/
+public class ImageMapChecker extends Checker {
+ private List maps;
+ private List mapNames;
+ private List imagesWithUsemapRefs;
+ private List usemapRefs;
+ private List listOfIds;
+ private String findingText;
+ private HtmlPage pageToCheck;
+
+
+ public ImageMapChecker(Configuration pConfig) {
+ super(pConfig);
+ }
+
+ @Override
+ protected void initCheckingResultsDescription() {
+ getCheckingResults().setWhatIsChecked("Consistency of ImageMaps");
+ getCheckingResults().setSourceItemName("imageMap");
+ getCheckingResults().setTargetItemName("map/area and usemap-references");
+ }
+
+ @Override
+ protected SingleCheckResults check(final HtmlPage pageToCheck) {
+
+ this.pageToCheck = pageToCheck;
+
+ readImageMapAttributesFromHtml();
+
+ checkBrokenImageMapReferences();
+
+ checkDuplicateMapNames();
+
+ checkDanglingMaps();
+
+ checkEmptyMaps();
+
+ checkForBrokenHrefLinks();// the major check
+
+ return getCheckingResults();
+ }
+
+ private void checkDanglingMaps() {
+ mapNames.stream()
+ .filter(n -> !usemapRefs.contains(n))
+ .map(mapName -> "ImageMap \"" + mapName + "\" not referenced by any image.")
+ .forEach(findingText -> getCheckingResults().addFinding(new Finding(findingText)));
+ }
+
+ private void checkEmptyMaps() {
+ mapNames.stream().map(mapName ->
+ pageToCheck.getAllAreasForMapName(mapName))
+ .filter(areas -> !areas.isEmpty())
+ .peek(a -> getCheckingResults().incNrOfChecks())
+ .forEach(area -> getCheckingResults().addFinding(new Finding(findingText)));
+ }
+
+ /*
+check for duplicate map names
+ */
+ private void checkDuplicateMapNames() {
+ int mapNameCount;
+
+ Set mapNameSet = new HashSet<>(mapNames);
+
+ mapNameSet.stream()
+ .peek(a -> getCheckingResults().incNrOfChecks())
+ .filter(name -> mapNames.stream().filter(name2 -> name2.equals(name)).count() > 1)
+ .forEach(mapName ->
+ getCheckingResults().addFinding(
+ new Finding(mapNames.stream().filter(name2 -> name2.equals(mapName)).count() + " imagemaps with identical name \"" + mapName + "\" exist.")));
+
+
+ }
+
+ /*
+ * ...
+ * a.) if there is no map named "y" -> problem
+ * b.) if there are more maps named "y" -> problem
+ */
+ private void checkBrokenImageMapReferences() {
+ imagesWithUsemapRefs.stream()
+ .forEach(imageTag -> checkBrokenImageMapReference(imageTag.getUsemapRef(), imageTag));
+ }
+
+ private void checkBrokenImageMapReference(String imgMap, HtmlElement imageTag) {
+ getCheckingResults().incNrOfChecks();
+
+
+ long mapCount = mapNames.stream().filter(it -> it == imgMap).count();
+
+ if (mapCount == 0L) {
+ // no map found, despite img-tag usemap-reference
+ findingText = "ImageMap \"" + imageTag.getUsemapRef() + "\" (referenced by image \"" + imageTag.getImageSrcAttribute() + "\") missing.";
+ getCheckingResults().addFinding(new Finding(findingText));
+ }
+ }
+
+ private void checkForBrokenHrefLinks() {
+
+ mapNames.forEach(n -> checkAreaHrefsForMapName(n));
+ }
+
+ /*
+ for a specific mapName, check all its contained areaHrefs
+ */
+ private void checkAreaHrefsForMapName(String mapName) {
+ List areaHrefs = pageToCheck.getAllHrefsForMapName(mapName);
+
+ // if this List is empty -> the map is empty
+ // TODO replace checkEmptyMaps with additional check here
+
+ areaHrefs.stream()
+ .peek(a -> getCheckingResults().incNrOfChecks())
+ .filter(href -> Web.isCrossReference(href))
+ .forEach(href -> checkLocalHref(href, mapName, areaHrefs));
+
+ }
+
+ /*
+check if href has valid local target
+TODO: currently restricted to LOCAL references
+TODO: remove duplication to BrokenCrossReferencesChecker
+*/
+ private void checkLocalHref(String href, String mapName, List areaHrefs) {
+ // strip href of its leading "#"
+ String linkTarget = (href.startsWith("#")) ? href.substring(1) : href;
+
+
+ if (!listOfIds.contains(linkTarget)) {
+
+ // we found a broken link!
+ findingText = "ImageMap \"" + mapName + "\" refers to missing link \"" + linkTarget + "\"";
+
+ // now count occurrences - how often is it referenced
+ int nrOfReferences = (int) areaHrefs.stream().filter(it -> it == href).count();
+ if (nrOfReferences > 1) {
+ findingText += ", reference count: " + nrOfReferences + ".";
+ } else findingText += ".";
+
+ getCheckingResults().newFinding(findingText, nrOfReferences);
+ }
+
+ }
+
+ private void readImageMapAttributesFromHtml() {
+ // get all
+ imagesWithUsemapRefs = pageToCheck.getImagesWithUsemapDeclaration();
+
+ // get all