Repository: crawler-commons/crawler-commons
Pull request #211 (2018-05-15 12:57:12)

Allow for legacy URIs when checking sitemap namespaces
- e.g., allow legacy namespace URI but ignore URLs
  from image and video sitemap extensions
- resolve relative namespace URIs
- add namespace URIs of sitemap extensions (news, images, videos)

E.g., to check the namespace but allow legacy URIs and also sitemaps without a namespace declaration:
```
sitemapParser.setStrictNamespace(true);
sitemapParser.addAcceptedNamespace(Namespace.SITEMAP_LEGACY);
sitemapParser.addAcceptedNamespace(Namespace.EMPTY);
```

To allow image URLs one could add
```
sitemapParser.addAcceptedNamespace(Namespace.IMAGE);
```
It's also easy to add any other namespace URI.


Patch:
diff --git a/CHANGES.txt b/CHANGES.txt
index 142941e..eb59157 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -15,6 +15,7 @@ Current Development 0.10-SNAPSHOT (yyyy-mm-dd)
 - Improve MIME detection for sitemaps (sebastian-nagel) #200
 - Make RobotRules accessible (aecio via kkrugler) #134
 - SimpleRobotRulesParser: Expose MAX_WARNINGS and MAX_CRAWL_DELAY (aecio via kkrugler) #194
+- Added main to SimpleRobotRulesParser for testing (sebastian-nagel) #193
 
 Release 0.9 (2017-10-27)
 - [Sitemaps] Removed DOM-based sitemap parser (jnioche) #177
diff --git a/src/main/java/crawlercommons/robots/BaseRobotRules.java b/src/main/java/crawlercommons/robots/BaseRobotRules.java
index 12dafd3..6e02f29 100644
--- a/src/main/java/crawlercommons/robots/BaseRobotRules.java
+++ b/src/main/java/crawlercommons/robots/BaseRobotRules.java
@@ -109,4 +109,29 @@ public abstract class BaseRobotRules implements Serializable {
         return true;
     }
 
+    /*
+     * (non-Javadoc)
+     * 
+     * @see java.lang.Object#equals(java.lang.Object)
+     */
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append(getClass()).append(":\n");
+        long delay = getCrawlDelay();
+        if (delay == UNSET_CRAWL_DELAY) {
+            sb.append(" - no crawl delay\n");
+        } else {
+            sb.append(" - crawl delay: ").append(delay).append('\n');
+        }
+        int nSitemaps = getSitemaps().size();
+        if (nSitemaps == 0) {
+            sb.append(" - no sitemap URLs\n");
+        } else {
+            sb.append(" - number of sitemap URLs: ").append(nSitemaps).append('\n');
+            sb.append("   sitemaps[0]: ").append(getSitemaps().get(0)).append('\n');
+        }
+        return sb.toString();
+    }
+
 }
diff --git a/src/main/java/crawlercommons/robots/SimpleRobotRules.java b/src/main/java/crawlercommons/robots/SimpleRobotRules.java
index c9d0e93..61657c6 100644
--- a/src/main/java/crawlercommons/robots/SimpleRobotRules.java
+++ b/src/main/java/crawlercommons/robots/SimpleRobotRules.java
@@ -254,8 +254,7 @@ public class SimpleRobotRules extends BaseRobotRules {
                 }
             } else {
                 // See if the pattern from patternPos to wildcardPos matches the
-                // text
-                // starting at textPos
+                // text starting at textPos
                 while ((patternPos < wildcardPos) && (textPos < textEnd)) {
                     if (text.charAt(textPos++) != pattern.charAt(patternPos++)) {
                         return false;
@@ -265,17 +264,14 @@ public class SimpleRobotRules extends BaseRobotRules {
         }
 
         // If we didn't reach the end of the pattern, make sure we're not at a
-        // wildcard, sa
-        // that's a 0 or more match, so then we're still OK.
+        // wildcard, that's a 0 or more match, so then we're still OK.
         while ((patternPos < patternEnd) && (pattern.charAt(patternPos) == '*')) {
             patternPos += 1;
         }
 
         // We're at the end, so we have a match if the pattern was completely
-        // consumed,
-        // and either we consumed all the text or we didn't have to match it all
-        // (no '$' at end
-        // of the pattern)
+        // consumed, and either we consumed all the text or we didn't have to
+        // match it all (no '$' at end of the pattern)
         return (patternPos == patternEnd) && ((textPos == textEnd) || !containsEndChar);
     }
 
@@ -345,4 +341,34 @@ public class SimpleRobotRules extends BaseRobotRules {
         return true;
     }
 
+    /*
+     * (non-Javadoc)
+     * 
+     * @see java.lang.Object#equals(java.lang.Object)
+     */
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append(super.toString());
+        int nRules = _rules.size();
+        if (nRules == 0) {
+            sb.append(" - no rules");
+            if (isAllowNone()) {
+                sb.append(" (allow none)");
+            } else if (isAllowAll()) {
+                sb.append(" (allow all)");
+            }
+            sb.append('\n');
+        } else {
+            sb.append(" - number of rules: ").append(nRules).append('\n');
+            if (nRules <= 10) {
+                for (int i = 0; i < nRules; i++) {
+                    RobotRule r = _rules.get(i);
+                    sb.append(r._allow ? "   A" : "   Disa").append("llow: ").append(r._prefix).append('\n');
+                }
+            }
+        }
+        return sb.toString();
+    }
+
 }
diff --git a/src/main/java/crawlercommons/robots/SimpleRobotRulesParser.java b/src/main/java/crawlercommons/robots/SimpleRobotRulesParser.java
index c4d24d4..4038caf 100644
--- a/src/main/java/crawlercommons/robots/SimpleRobotRulesParser.java
+++ b/src/main/java/crawlercommons/robots/SimpleRobotRulesParser.java
@@ -16,7 +16,11 @@
 
 package crawlercommons.robots;
 
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
 import java.net.URL;
+import java.net.URLConnection;
 import java.net.URLDecoder;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
@@ -27,6 +31,7 @@ import java.util.StringTokenizer;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import org.apache.commons.io.IOUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -161,10 +166,8 @@ public class SimpleRobotRulesParser extends BaseRobotsParser {
         private boolean _finishedAgentFields;
 
         // True if we're done adding rules for a matched (not wildcard) agent
-        // name.
-        // When this is true, we only consider sitemap directives, so we skip
-        // all
-        // remaining user agent blocks.
+        // name. When this is true, we only consider sitemap directives, so we
+        // skip all remaining user agent blocks.
         private boolean _skipAgents;
 
         private String _url;
@@ -369,16 +372,13 @@ public class SimpleRobotRulesParser extends BaseRobotsParser {
             throw new IllegalStateException("Can't use status code constructor with 2xx response");
         } else if ((httpStatusCode >= 300) && (httpStatusCode < 400)) {
             // Should only happen if we're getting endless redirects (more than
-            // our follow limit), so
-            // treat it as a temporary failure.
+            // our follow limit), so treat it as a temporary failure.
             result = new SimpleRobotRules(RobotRulesMode.ALLOW_NONE);
             result.setDeferVisits(true);
         } else if ((httpStatusCode >= 400) && (httpStatusCode < 500)) {
             // Some sites return 410 (gone) instead of 404 (not found), so treat
-            // as the same.
-            // Actually treat all (including forbidden) as "no robots.txt", as
-            // that's what Google
-            // and other search engines do.
+            // as the same. Actually treat all (including forbidden) as "no
+            // robots.txt", as that's what Google and other search engines do.
             result = new SimpleRobotRules(RobotRulesMode.ALLOW_ALL);
         } else {
             // Treat all other status codes as a temporary failure.
@@ -787,4 +787,58 @@ public class SimpleRobotRulesParser extends BaseRobotsParser {
         _maxCrawlDelay = maxCrawlDelay;
     }
 
+    public static void main(String[] args) throws MalformedURLException, IOException {
+        if (args.length < 1) {
+            System.err.println("SimpleRobotRulesParser <robots.txt> [[<agentname>] <URL>...]");
+            System.err.println();
+            System.err.println("Parse a robots.txt file");
+            System.err.println("  <robots.txt>\tURL pointing to robots.txt file.");
+            System.err.println("              \tTo read a local file use a file:// URL");
+            System.err.println("              \t(parsed as http://example.com/robots.txt)");
+            System.err.println("  <agentname> \tuser agent name to check for exclusion rules.");
+            System.err.println("              \tIf not defined check with '*'");
+            System.err.println("  <URL>       \tcheck URL whether allowed or forbidden.");
+            System.err.println("              \tIf no URL is given show robots.txt rules");
+            System.exit(1);
+        }
+
+        String url = args[0];
+        String agentName = "*";
+        if (args.length >= 2) {
+            agentName = args[1];
+        }
+
+        SimpleRobotRulesParser parser = new SimpleRobotRulesParser();
+        BaseRobotRules rules;
+        URLConnection connection = new URL(url).openConnection();
+        try {
+            byte[] content = IOUtils.toByteArray(connection);
+            if (!url.matches("^https?://")) {
+                // use artificial URL to avoid problems resolving relative
+                // sitemap paths for file:/ URLs
+                url = "http://example.com/robots.txt";
+            }
+            rules = parser.parseContent(url, content, "text/plain", agentName);
+        } catch (IOException e) {
+            if (connection instanceof HttpURLConnection) {
+                int code = ((HttpURLConnection) connection).getResponseCode();
+                rules = parser.failedFetch(code);
+                System.out.println("Fetch of " + url + " failed with HTTP status code " + code);
+            } else {
+                throw e;
+            }
+        }
+
+        if (args.length < 3) {
+            // no URL(s) given, print rules and exit
+            System.out.println("Robot rules for user agentname '" + agentName + "':");
+            System.out.println(rules.toString());
+        } else {
+            System.out.println("Checking URLs:");
+            for (int i = 2; i < args.length; i++) {
+                System.out.println((rules.isAllowed(args[i]) ? "allowed  " : "forbidden") + "\t" + args[i]);
+            }
+        }
+    }
+
 }
diff --git a/src/main/java/crawlercommons/sitemaps/Namespace.java b/src/main/java/crawlercommons/sitemaps/Namespace.java
index 3789f8a..e69817e 100644
--- a/src/main/java/crawlercommons/sitemaps/Namespace.java
+++ b/src/main/java/crawlercommons/sitemaps/Namespace.java
@@ -16,6 +16,10 @@
 
 package crawlercommons.sitemaps;
 
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
 /**
  * supported sitemap formats:
  * https://www.sitemaps.org/protocol.html#otherformats
@@ -24,6 +28,50 @@ public class Namespace {
 
     public static final String SITEMAP = "http://www.sitemaps.org/schemas/sitemap/0.9";
 
+    /**
+     * Legacy schema URIs from prior sitemap protocol versions and frequent
+     * variants.
+     */
+    public static final String[] SITEMAP_LEGACY = { //
+                    "https://www.sitemaps.org/schemas/sitemap/0.9", //
+                    "http://www.sitemaps.org/schemas/sitemap/0.9/", //
+                    "https://www.sitemaps.org/schemas/sitemap/0.9/", //
+                    "http://www.google.com/schemas/sitemap/0.9", //
+                    "https://www.google.com/schemas/sitemap/0.9", //
+                    "http://www.google.com/schemas/sitemap/0.84", //
+                    "https://www.google.com/schemas/sitemap/0.84", //
+                    "http://www.google.com/schemas/sitemap/0.90", //
+                    "https://sitemaps.org/schemas/sitemap/0.9",
+                    };
+
+    public static final String[] IMAGE = { //
+                    "http://www.google.com/schemas/sitemap-image/1.1", //
+                    "https://www.google.com/schemas/sitemap-image/1.1" //
+                    };
+
+    public static final String[] VIDEO = { //
+                    "http://www.google.com/schemas/sitemap-video/1.1", //
+                    "https://www.google.com/schemas/sitemap-video/1.1" //
+    };
+
+    public static final String[] NEWS = { //
+                    "http://www.google.com/schemas/sitemap-news/0.9", //
+                    "https://www.google.com/schemas/sitemap-news/0.9", //
+                    "http://www.google.com/schemas/sitemap-news/0.84" //
+    };
+
+    public static final String LINKS = "http://www.w3.org/1999/xhtml";
+
+    /**
+     * In contradiction to the protocol specification ("The Sitemap must ...
+     * [s]pecify the namespace (protocol standard) within the &lt;urlset&gt;
+     * tag."), some sitemaps do not define a (default) namespace.
+     * 
+     * By accepting the "empty" namespace, you'll get URLs even from those
+     * sitemaps.
+     */
+    public static final String EMPTY = "";
+
     /**
      * RSS and Atom sitemap formats do not have strict definition. But if we do
      * not parse as namespace aware, then RSS/Atom files that choose to use
@@ -35,4 +83,24 @@ public class Namespace {
     public static final String ATOM_0_3 = "http://purl.org/atom/ns#";
     public static final String ATOM_1_0 = "http://www.w3.org/2005/Atom";
 
+    public static final Set<String> SITEMAP_SUPPORTED_NAMESPACES = new HashSet<>();
+    static {
+        SITEMAP_SUPPORTED_NAMESPACES.add(SITEMAP);
+        SITEMAP_SUPPORTED_NAMESPACES.addAll(Arrays.asList(SITEMAP_LEGACY));
+        SITEMAP_SUPPORTED_NAMESPACES.addAll(Arrays.asList(IMAGE));
+        SITEMAP_SUPPORTED_NAMESPACES.addAll(Arrays.asList(VIDEO));
+        SITEMAP_SUPPORTED_NAMESPACES.addAll(Arrays.asList(NEWS));
+        SITEMAP_SUPPORTED_NAMESPACES.add(LINKS);
+    }
+
+    /**
+     * @param uri
+     *            URI string identifying the namespace
+     * @return true if namespace (identified by URI) is supported, false if the
+     *         namespace is not supported or unknown
+     */
+    public static boolean isSupported(String uri) {
+        return SITEMAP_SUPPORTED_NAMESPACES.contains(uri);
+    }
+
 }
diff --git a/src/main/java/crawlercommons/sitemaps/SiteMapParser.java b/src/main/java/crawlercommons/sitemaps/SiteMapParser.java
index 7be584b..e7c634a 100644
--- a/src/main/java/crawlercommons/sitemaps/SiteMapParser.java
+++ b/src/main/java/crawlercommons/sitemaps/SiteMapParser.java
@@ -28,7 +28,9 @@ import java.io.StringReader;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.Collection;
+import java.util.HashSet;
 import java.util.Locale;
+import java.util.Set;
 import java.util.function.Consumer;
 import java.util.zip.GZIPInputStream;
 
@@ -79,6 +81,9 @@ public class SiteMapParser {
      **/
     protected boolean strictNamespace = false;
 
+    /** Set of namespaces (if {@link #strictNamespace}) accepted by the parser. URLs from other namespaces are ignored. */
+    protected Set<String> acceptedNamespaces = new HashSet<>();
+
     private MimeTypeDetector mimeTypeDetector;
 
     public SiteMapParser() {
@@ -106,7 +111,8 @@ public class SiteMapParser {
 
     /**
      * @return whether the parser allows any namespace or just the one from the
-     *         specification
+     *         specification (or any namespace accepted,
+     *         {@link #addAcceptedNamespace(String)})
      */
     public boolean isStrictNamespace() {
         return strictNamespace;
@@ -114,10 +120,37 @@ public class SiteMapParser {
 
     /**
      * Sets the parser to allow any namespace or just the one from the
-     * specification
+     * specification, or any accepted namespace (see
+     * {@link #addAcceptedNamespace(String)}). Note enabling strict namespace
+     * checking always adds the namespace defined by the current sitemap
+     * specificiation ({@link Namespace#SITEMAP}) to the list of accepted
+     * namespaces.
      */
     public void setStrictNamespace(boolean s) {
         strictNamespace = s;
+        if (strictNamespace) {
+            addAcceptedNamespace(Namespace.SITEMAP);
+        }
+    }
+
+    /**
+     * Add namespace URI to set of accepted namespaces.
+     * 
+     * @param namespaceUri
+     */
+    public void addAcceptedNamespace(String namespaceUri) {
+        acceptedNamespaces.add(namespaceUri);
+    }
+
+    /**
+     * Add namespace URIs to set of accepted namespaces.
+     * 
+     * @param namespaceUris
+     */
+    public void addAcceptedNamespace(String[] namespaceUris) {
+        for (String namespaceUri : namespaceUris) {
+            acceptedNamespaces.add(namespaceUri);
+        }
     }
 
     /**
@@ -467,7 +500,13 @@ public class SiteMapParser {
         } catch (Exception e) {
             throw new RuntimeException("Failed to configure XML parser: " + e.toString());
         }
+
         DelegatorHandler handler = new DelegatorHandler(sitemapUrl, strict);
+        handler.setStrictNamespace(isStrictNamespace());
+        if (isStrictNamespace()) {
+            handler.setAcceptedNamespaces(acceptedNamespaces);
+        }
+
         try {
             SAXParser saxParser = factory.newSAXParser();
             saxParser.getXMLReader().setEntityResolver(new EntityResolver() {
@@ -477,7 +516,6 @@ public class SiteMapParser {
                     return new InputSource(new StringReader(""));
                 }
             });
-            handler.setStrictNamespace(isStrictNamespace());
             saxParser.parse(is, handler);
             AbstractSiteMap sitemap = handler.getSiteMap();
             if (sitemap == null) {
diff --git a/src/main/java/crawlercommons/sitemaps/sax/DelegatorHandler.java b/src/main/java/crawlercommons/sitemaps/sax/DelegatorHandler.java
index 3a028e0..3073cd9 100644
--- a/src/main/java/crawlercommons/sitemaps/sax/DelegatorHandler.java
+++ b/src/main/java/crawlercommons/sitemaps/sax/DelegatorHandler.java
@@ -16,8 +16,12 @@
 
 package crawlercommons.sitemaps.sax;
 
+import static crawlercommons.sitemaps.SiteMapParser.LOG;
+
+import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.LinkedList;
+import java.util.Set;
 
 import org.xml.sax.Attributes;
 import org.xml.sax.SAXException;
@@ -40,6 +44,7 @@ public class DelegatorHandler extends DefaultHandler {
     private boolean strict;
     private boolean strictNamespace;
     private UnknownFormatException exception;
+    private Set<String> acceptedNamespaces;
 
     protected DelegatorHandler(LinkedList<String> elementStack, boolean strict) {
         this.elementStack = elementStack;
@@ -60,22 +65,22 @@ public class DelegatorHandler extends DefaultHandler {
         return strict;
     }
 
-    /**
-     * @return whether the parser allows any namespace or just the one from the
-     *         specification
-     */
-    public boolean isStrictNamespace() {
+    protected boolean isStrictNamespace() {
         return strictNamespace;
     }
 
-    /**
-     * Sets the parser to allow any namespace or just the one from the
-     * specification
-     */
     public void setStrictNamespace(boolean s) {
         strictNamespace = s;
     }
 
+    public void setAcceptedNamespaces(Set<String> acceptedSet) {
+        acceptedNamespaces = acceptedSet;
+    }
+
+    protected boolean isAcceptedNamespace(String uri) {
+        return acceptedNamespaces.contains(uri);
+    }
+
     protected void setException(UnknownFormatException exception) {
         this.exception = exception;
     }
@@ -109,17 +114,44 @@ public class DelegatorHandler extends DefaultHandler {
         // and also RSS 1.0 specification http://web.resource.org/rss/1.0/spec
         else if ("channel".equals(localName)) {
             delegate = new RSSHandler(url, elementStack, strict);
-        } else if (isStrictNamespace() && !Namespace.SITEMAP.equals(uri)) {
-            setException(new UnknownFormatException("Namespace " + uri + " does not match standard namespace " + Namespace.SITEMAP));
-            return;
         } else if ("sitemapindex".equals(localName)) {
             delegate = new XMLIndexHandler(url, elementStack, strict);
         } else if ("urlset".equals(localName)) {
             delegate = new XMLHandler(url, elementStack, strict);
+        } else {
+            LOG.debug("Skipped unknown root element <{}> in {}", localName, url);
+            return;
         }
-        if (delegate != null) {
-            // configure delegate
-            delegate.setStrictNamespace(isStrictNamespace());
+        // configure delegate
+        delegate.setStrictNamespace(isStrictNamespace());
+        delegate.setAcceptedNamespaces(acceptedNamespaces);
+        // validate XML namespace
+        if (isStrictNamespace()) {
+            if (delegate instanceof AtomHandler || delegate instanceof RSSHandler) {
+                // no namespace checking for feeds
+                return;
+            }
+            if (!isAcceptedNamespace(uri) && uri.startsWith("/")) {
+                // first, try to resolve relative namespace URI (deprecated but
+                // not forbidden), e.g., //www.sitemaps.org/schemas/sitemap/0.9
+                try {
+                    URL u = new URL(url, uri);
+                    uri = u.toString();
+                } catch (MalformedURLException e) {
+                    LOG.warn("Failed to resolve relative namespace URI {} in sitemap {}", uri, url);
+                }
+            }
+            if (!isAcceptedNamespace(uri)) {
+                String msg;
+                if (!Namespace.isSupported(uri)) {
+                    msg = "Unsupported namespace <" + uri + ">";
+                } else {
+                    msg = "Namespace <" + uri + "> not accepted";
+                }
+                setException(new UnknownFormatException(msg));
+                delegate = null;
+                return;
+            }
         }
     }
 
diff --git a/src/main/java/crawlercommons/sitemaps/sax/XMLHandler.java b/src/main/java/crawlercommons/sitemaps/sax/XMLHandler.java
index 67f3d02..7553d62 100644
--- a/src/main/java/crawlercommons/sitemaps/sax/XMLHandler.java
+++ b/src/main/java/crawlercommons/sitemaps/sax/XMLHandler.java
@@ -29,7 +29,6 @@ import org.xml.sax.SAXParseException;
 
 import crawlercommons.sitemaps.AbstractSiteMap;
 import crawlercommons.sitemaps.AbstractSiteMap.SitemapType;
-import crawlercommons.sitemaps.Namespace;
 import crawlercommons.sitemaps.SiteMap;
 import crawlercommons.sitemaps.SiteMapURL;
 
@@ -72,7 +71,8 @@ class XMLHandler extends DelegatorHandler {
     }
 
     public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
-        if (isStrictNamespace() && !Namespace.SITEMAP.equals(uri)) {
+        if (isStrictNamespace() && !isAcceptedNamespace(uri)) {
+            LOG.debug("Skip element <{}>, namespace <{}> not accepted", localName, uri);
             currentElementNamespaceIsValid = false;
             return;
         }
@@ -98,7 +98,7 @@ class XMLHandler extends DelegatorHandler {
     }
 
     public void endElement(String uri, String localName, String qName) throws SAXException {
-        if (isStrictNamespace() && !Namespace.SITEMAP.equals(uri)) {
+        if (isStrictNamespace() && !isAcceptedNamespace(uri)) {
             return;
         }
         if ("url".equals(localName) && "urlset".equals(currentElementParent())) {
diff --git a/src/main/java/crawlercommons/sitemaps/sax/XMLIndexHandler.java b/src/main/java/crawlercommons/sitemaps/sax/XMLIndexHandler.java
index ba3e65a..84d87ca 100644
--- a/src/main/java/crawlercommons/sitemaps/sax/XMLIndexHandler.java
+++ b/src/main/java/crawlercommons/sitemaps/sax/XMLIndexHandler.java
@@ -28,7 +28,6 @@ import org.xml.sax.SAXException;
 import org.xml.sax.SAXParseException;
 
 import crawlercommons.sitemaps.AbstractSiteMap;
-import crawlercommons.sitemaps.Namespace;
 import crawlercommons.sitemaps.SiteMap;
 import crawlercommons.sitemaps.SiteMapIndex;
 import crawlercommons.sitemaps.AbstractSiteMap.SitemapType;
@@ -71,7 +70,7 @@ class XMLIndexHandler extends DelegatorHandler {
     }
 
     public void endElement(String uri, String localName, String qName) throws SAXException {
-        if (isStrictNamespace() && !Namespace.SITEMAP.equals(uri)) {
+        if (isStrictNamespace() && !isAcceptedNamespace(uri)) {
             return;
         }
         if ("sitemap".equals(currentElement())) {


Test patch:
diff --git a/src/test/java/crawlercommons/sitemaps/SiteMapParserTest.java b/src/test/java/crawlercommons/sitemaps/SiteMapParserTest.java
index ae3194c..f64c536 100644
--- a/src/test/java/crawlercommons/sitemaps/SiteMapParserTest.java
+++ b/src/test/java/crawlercommons/sitemaps/SiteMapParserTest.java
@@ -145,7 +145,7 @@ public class SiteMapParserTest {
             asm = parser.parseSiteMap(content, url);
             fail("Expected an UnknownFormatException because of wrong namespace");
         } catch (UnknownFormatException e) {
-            assertTrue(e.getMessage().contains("does not match standard namespace"));
+            assertTrue(e.getMessage().matches("Namespace .* not accepted"));
         }
 
         // try again in lenient mode