Merge pull request #33 from sergiovm/master

added new generator for google link extension
This commit is contained in:
Matthias Kurz 2018-07-05 15:05:53 +02:00 committed by GitHub
commit b63ffc081e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 356 additions and 0 deletions

View File

@ -0,0 +1,135 @@
package com.redfin.sitemapgenerator;
import java.io.File;
import java.net.*;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
/**
* Builds a Google Link Sitemap (to indicate alternate language pages).
*
* @author Sergio Vico
* @see <a href="https://support.google.com/webmasters/answer/2620865">Creating alternate language pages Sitemaps</a>
* @see <a href="https://developers.google.com/search/mobile-sites/mobile-seo/separate-urls?hl=en">Mobile SEO configurations | Separate URLs </a>
*/
public class GoogleLinkSitemapGenerator extends SitemapGenerator<GoogleLinkSitemapUrl, GoogleLinkSitemapGenerator> {
private static class Renderer extends AbstractSitemapUrlRenderer<GoogleLinkSitemapUrl>
implements ISitemapUrlRenderer<GoogleLinkSitemapUrl> {
public Class<GoogleLinkSitemapUrl> getUrlClass() {
return GoogleLinkSitemapUrl.class;
}
public String getXmlNamespaces() {
return "xmlns:xhtml=\"http://www.w3.org/1999/xhtml\"";
}
public void render(final GoogleLinkSitemapUrl url, final StringBuilder sb, final W3CDateFormat dateFormat) {
final StringBuilder tagSb = new StringBuilder();
for (final Entry<URI, Map<String, String>> entry : url.getAlternates().entrySet()) {
tagSb.append(" <xhtml:link\n");
tagSb.append(" rel=\"alternate\"\n");
for(final Entry<String, String> innerEntry : entry.getValue().entrySet()){
tagSb.append(" " + innerEntry.getKey() + "=\"" + innerEntry.getValue() + "\"\n");
}
tagSb.append(" href=\"" + UrlUtils.escapeXml(entry.getKey().toString()) + "\"\n");
tagSb.append(" />\n");
}
super.render(url, sb, dateFormat, tagSb.toString());
}
}
/**
* Configures a builder so you can specify sitemap generator options
*
* @param baseUrl
* All URLs in the generated sitemap(s) should appear under this base URL
* @param baseDir
* Sitemap files will be generated in this directory as either "sitemap.xml" or
* "sitemap1.xml" "sitemap2.xml" and so on.
* @return a builder; call .build() on it to make a sitemap generator
*/
public static SitemapGeneratorBuilder<GoogleLinkSitemapGenerator> builder(final String baseUrl, final File baseDir)
throws MalformedURLException {
return new SitemapGeneratorBuilder<GoogleLinkSitemapGenerator>(baseUrl, baseDir,
GoogleLinkSitemapGenerator.class);
}
/**
* Configures a builder so you can specify sitemap generator options
*
* @param baseUrl
* All URLs in the generated sitemap(s) should appear under this base URL
* @param baseDir
* Sitemap files will be generated in this directory as either "sitemap.xml" or
* "sitemap1.xml" "sitemap2.xml" and so on.
* @return a builder; call .build() on it to make a sitemap generator
*/
public static SitemapGeneratorBuilder<GoogleLinkSitemapGenerator> builder(final URL baseUrl, final File baseDir) {
return new SitemapGeneratorBuilder<GoogleLinkSitemapGenerator>(baseUrl, baseDir,
GoogleLinkSitemapGenerator.class);
}
/**
* Configures the generator with a base URL and a null directory. The object constructed is not
* intended to be used to write to files. Rather, it is intended to be used to obtain
* XML-formatted strings that represent sitemaps.
*
* @param baseUrl
* All URLs in the generated sitemap(s) should appear under this base URL
*/
public GoogleLinkSitemapGenerator(final String baseUrl) throws MalformedURLException {
this(new SitemapGeneratorOptions(new URL(baseUrl)));
}
/**
* Configures the generator with a base URL and directory to write the sitemap files.
*
* @param baseUrl
* All URLs in the generated sitemap(s) should appear under this base URL
* @param baseDir
* Sitemap files will be generated in this directory as either "sitemap.xml" or
* "sitemap1.xml" "sitemap2.xml" and so on.
* @throws MalformedURLException
*/
public GoogleLinkSitemapGenerator(final String baseUrl, final File baseDir) throws MalformedURLException {
this(new SitemapGeneratorOptions(baseUrl, baseDir));
}
/**
* Configures the generator with a base URL and a null directory. The object constructed is not
* intended to be used to write to files. Rather, it is intended to be used to obtain
* XML-formatted strings that represent sitemaps.
*
* @param baseUrl
* All URLs in the generated sitemap(s) should appear under this base URL
*/
public GoogleLinkSitemapGenerator(final URL baseUrl) {
this(new SitemapGeneratorOptions(baseUrl));
}
/**
* Configures the generator with a base URL and directory to write the sitemap files.
*
* @param baseUrl
* All URLs in the generated sitemap(s) should appear under this base URL
* @param baseDir
* Sitemap files will be generated in this directory as either "sitemap.xml" or
* "sitemap1.xml" "sitemap2.xml" and so on.
*/
public GoogleLinkSitemapGenerator(final URL baseUrl, final File baseDir) {
this(new SitemapGeneratorOptions(baseUrl, baseDir));
}
GoogleLinkSitemapGenerator(final AbstractSitemapGeneratorOptions<?> options) {
super(options, new Renderer());
}
}

View File

@ -0,0 +1,102 @@
package com.redfin.sitemapgenerator;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* One configurable Google Link URL. To configure, use {@link Options}
*
* @author Sergio Vico
* @see Options
* @see <a href="https://support.google.com/webmasters/answer/2620865">Creating alternate language pages Sitemaps</a>
* @see <a href="https://developers.google.com/search/mobile-sites/mobile-seo/separate-urls?hl=en">Mobile SEO configurations | Separate URLs </a>
*/
public class GoogleLinkSitemapUrl extends WebSitemapUrl {
/** Options to configure URLs with alternates */
public static class Options extends AbstractSitemapUrlOptions<GoogleLinkSitemapUrl, Options> {
private final Map<URI, Map<String, String>> alternates;
private static Map<URI, Map<String, String>> convertAlternates(final Map<String, Map<String, String>> alternates)
throws URISyntaxException {
final Map<URI, Map<String, String>> converted = new LinkedHashMap<URI, Map<String, String>>();
for (final Map.Entry<String, Map<String, String>> entry : alternates.entrySet()) {
converted.put(new URI(entry.getKey()), new LinkedHashMap<String, String>(entry.getValue()));
}
return converted;
}
/**
* Options constructor with the alternates configurations
*
* @param url Base URL into which we will be adding alternates
* @param alternates Map&lt;String, Map&lt;String, String&gt;&gt; where the key is the href and
* the value is a generic Map&lt;String, String&gt; holding the attributes of
* the link (e.g. hreflang, media, ...)
*/
public Options(final String url, final Map<String, Map<String, String>> alternates) throws URISyntaxException, MalformedURLException {
this(new URL(url), convertAlternates(alternates));
}
/**
* Options constructor with the alternates configurations
*
* @param url Base URL into which we will be adding alternates
* @param alternates Map&lt;URL, Map&lt;String, String&gt;&gt; where the key is the href and
* the value is a generic Map&lt;String, String&gt; holding the attributes of
* the link (e.g. hreflang, media, ...)
*/
public Options(final URL url, final Map<URI, Map<String, String>> alternates) {
super(url, GoogleLinkSitemapUrl.class);
this.alternates = new LinkedHashMap<URI, Map<String, String>>(alternates);
}
}
private final Map<URI, Map<String, String>> alternates;
/**
* Constructor specifying the URL and the alternates configurations with Options object
*
* @param options Configuration object to initialize the GoogleLinkSitemapUrl with.
* @see Options#Options(java.lang.String, java.util.Map)
*/
public GoogleLinkSitemapUrl(final Options options) {
super(options);
alternates = options.alternates;
}
/**
* Constructor specifying the URL as a String and the alternates configurations
*
* @param url Base URL into which we will be adding alternates
* @param alternates Map&lt;String, Map&lt;String, String&gt;&gt; where the key is the href and
* the value is a generic Map&lt;String, String&gt; holding the attributes of
* the link (e.g. hreflang, media, ...)
*/
public GoogleLinkSitemapUrl(final String url, final Map<String, Map<String, String>> alternates) throws URISyntaxException, MalformedURLException {
this(new Options(url, alternates));
}
/**
* Constructor specifying the URL as a URL and the alternates configurations
*
* @param url Base URL into which we will be adding alternates
* @param alternates Map&lt;String, Map&lt;String, String&gt;&gt; where the key is the href and
* the value is a generic Map&lt;String, String&gt; holding the attributes of
* the link (e.g. hreflang, media, ...)
*/
public GoogleLinkSitemapUrl(final URL url, final Map<URI, Map<String, String>> alternates) {
this(new Options(url, alternates));
}
public Map<URI, Map<String, String>> getAlternates() {
return this.alternates;
}
}

View File

@ -0,0 +1,117 @@
package com.redfin.sitemapgenerator;
import java.io.File;
import java.util.*;
import junit.framework.TestCase;
public class GoogleLinkSitemapUrlTest extends TestCase {
File dir;
GoogleLinkSitemapGenerator wsg;
@Override
public void setUp() throws Exception {
dir = File.createTempFile(GoogleLinkSitemapUrlTest.class.getSimpleName(), "");
dir.delete();
dir.mkdir();
dir.deleteOnExit();
}
@Override
public void tearDown() {
wsg = null;
for (final File file : dir.listFiles()) {
file.deleteOnExit();
file.delete();
}
dir.delete();
dir = null;
}
public void testSimpleUrlWithHrefLang() throws Exception {
wsg = new GoogleLinkSitemapGenerator("http://www.example.com", dir);
final Map<String, Map<String, String>> alternates = new LinkedHashMap<String, Map<String, String>>();
alternates.put("http://www.example/en/index.html", Collections.singletonMap("hreflang", "en-GB"));
alternates.put("http://www.example/fr/index.html", Collections.singletonMap("hreflang", "fr-FR"));
alternates.put("http://www.example/es/index.html", Collections.singletonMap("hreflang", "es-ES"));
final GoogleLinkSitemapUrl url = new GoogleLinkSitemapUrl("http://www.example.com/index.html", alternates);
wsg.addUrl(url);
//@formatter:off
final String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" "
+ "xmlns:xhtml=\"http://www.w3.org/1999/xhtml\" >\n"
+ " <url>\n"
+ " <loc>http://www.example.com/index.html</loc>\n"
+ " <xhtml:link\n"
+ " rel=\"alternate\"\n"
+ " hreflang=\"en-GB\"\n"
+ " href=\"http://www.example/en/index.html\"\n"
+ " />\n"
+ " <xhtml:link\n"
+ " rel=\"alternate\"\n"
+ " hreflang=\"fr-FR\"\n"
+ " href=\"http://www.example/fr/index.html\"\n"
+ " />\n"
+ " <xhtml:link\n"
+ " rel=\"alternate\"\n"
+ " hreflang=\"es-ES\"\n"
+ " href=\"http://www.example/es/index.html\"\n"
+ " />\n"
+ " </url>\n"
+ "</urlset>";
//@formatter:on
final String sitemap = writeSingleSiteMap(wsg);
assertEquals(expected, sitemap);
}
public void testSimpleUrlWithMedia() throws Exception {
wsg = new GoogleLinkSitemapGenerator("http://www.example.com", dir);
final Map<String, Map<String, String>> alternates = new LinkedHashMap<String, Map<String, String>>();
alternates.put("http://www.example/en/index.html", Collections.singletonMap("media", "only screen and (max-width: 640px)"));
alternates.put("http://www.example/fr/index.html", Collections.singletonMap("media", "only screen and (max-width: 640px)"));
alternates.put("http://www.example/es/index.html", Collections.singletonMap("media", "only screen and (max-width: 640px)"));
final GoogleLinkSitemapUrl url = new GoogleLinkSitemapUrl("http://www.example.com/index.html", alternates);
wsg.addUrl(url);
//@formatter:off
final String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" "
+ "xmlns:xhtml=\"http://www.w3.org/1999/xhtml\" >\n"
+ " <url>\n"
+ " <loc>http://www.example.com/index.html</loc>\n"
+ " <xhtml:link\n"
+ " rel=\"alternate\"\n"
+ " media=\"only screen and (max-width: 640px)\"\n"
+ " href=\"http://www.example/en/index.html\"\n"
+ " />\n"
+ " <xhtml:link\n"
+ " rel=\"alternate\"\n"
+ " media=\"only screen and (max-width: 640px)\"\n"
+ " href=\"http://www.example/fr/index.html\"\n"
+ " />\n"
+ " <xhtml:link\n"
+ " rel=\"alternate\"\n"
+ " media=\"only screen and (max-width: 640px)\"\n"
+ " href=\"http://www.example/es/index.html\"\n"
+ " />\n"
+ " </url>\n"
+ "</urlset>";
//@formatter:on
final String sitemap = writeSingleSiteMap(wsg);
assertEquals(expected, sitemap);
}
private String writeSingleSiteMap(final GoogleLinkSitemapGenerator wsg) {
final List<File> files = wsg.write();
assertEquals("Too many files: " + files.toString(), 1, files.size());
assertEquals("Sitemap misnamed", "sitemap.xml", files.get(0).getName());
return TestUtil.slurpFileAndDelete(files.get(0));
}
}

View File

@ -315,6 +315,7 @@ public class SitemapGeneratorTest extends TestCase {
while ((c = reader.read()) != -1) {
sb.append((char)c);
}
reader.close();
} catch (IOException e) {
throw new RuntimeException(e);
}

View File

@ -32,6 +32,7 @@ public class TestUtil {
while ((c = reader.read()) != -1) {
sb.append((char)c);
}
reader.close();
} catch (IOException e) {
throw new RuntimeException(e);
}