diff --git a/src/main/java/com/redfin/sitemapgenerator/GoogleImageSitemapGenerator.java b/src/main/java/com/redfin/sitemapgenerator/GoogleImageSitemapGenerator.java
new file mode 100644
index 0000000..6961a6a
--- /dev/null
+++ b/src/main/java/com/redfin/sitemapgenerator/GoogleImageSitemapGenerator.java
@@ -0,0 +1,103 @@
+package com.redfin.sitemapgenerator;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+/**
+ * Builds a sitemap for Google Image search. To configure options use {@link #builder(URL, File)}
+ * @see Manage your sitemaps
+ * */
+public class GoogleImageSitemapGenerator extends SitemapGenerator {
+
+ GoogleImageSitemapGenerator(AbstractSitemapGeneratorOptions> options) {
+ super(options, new GoogleImageSitemapGenerator.Renderer());
+ }
+
+ /** 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 GoogleImageSitemapGenerator(String baseUrl, File baseDir)
+ throws MalformedURLException {
+ this(new SitemapGeneratorOptions(baseUrl, baseDir));
+ }
+
+ /**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 GoogleImageSitemapGenerator(URL baseUrl, File baseDir) {
+ 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 GoogleImageSitemapGenerator(String baseUrl) throws MalformedURLException {
+ this(new SitemapGeneratorOptions(new URL(baseUrl)));
+ }
+
+ /**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 GoogleImageSitemapGenerator(URL baseUrl) {
+ this(new SitemapGeneratorOptions(baseUrl));
+ }
+
+ /** 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 builder(URL baseUrl, File baseDir) {
+ return new SitemapGeneratorBuilder(baseUrl, baseDir, GoogleImageSitemapGenerator.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
+ * @throws MalformedURLException
+ */
+ public static SitemapGeneratorBuilder builder(String baseUrl, File baseDir) throws MalformedURLException {
+ return new SitemapGeneratorBuilder(baseUrl, baseDir, GoogleImageSitemapGenerator.class);
+ }
+
+ private static class Renderer extends AbstractSitemapUrlRenderer implements ISitemapUrlRenderer {
+
+ public Class getUrlClass() {
+ return GoogleImageSitemapUrl.class;
+ }
+
+ public String getXmlNamespaces() {
+ return "xmlns:image=\"http://www.google.com/schemas/sitemap-image/1.1\"";
+ }
+
+ public void render(GoogleImageSitemapUrl url, StringBuilder sb, W3CDateFormat dateFormat) {
+ StringBuilder tagSb = new StringBuilder();
+
+ for(Image image : url.getImages()) {
+ tagSb.append(" \n");
+ renderTag(tagSb, "image", "loc", image.getUrl());
+ renderTag(tagSb, "image", "caption", image.getCaption());
+ renderTag(tagSb, "image", "title", image.getTitle());
+ renderTag(tagSb, "image", "geo_location", image.getGeoLocation());
+ renderTag(tagSb, "image", "license", image.getLicense());
+ tagSb.append(" \n");
+ }
+ super.render(url, sb, dateFormat, tagSb.toString());
+ }
+ }
+}
diff --git a/src/main/java/com/redfin/sitemapgenerator/GoogleImageSitemapUrl.java b/src/main/java/com/redfin/sitemapgenerator/GoogleImageSitemapUrl.java
new file mode 100644
index 0000000..5e18c80
--- /dev/null
+++ b/src/main/java/com/redfin/sitemapgenerator/GoogleImageSitemapUrl.java
@@ -0,0 +1,74 @@
+package com.redfin.sitemapgenerator;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/** One configurable Google Image Search URL. To configure, use {@link Options}
+ *
+ * @see Options
+ * @see Creating Image Sitemaps
+ */
+public class GoogleImageSitemapUrl extends WebSitemapUrl {
+
+ private final List images;
+
+ public GoogleImageSitemapUrl(String url) throws MalformedURLException {
+ this(new Options(url));
+ }
+
+ public GoogleImageSitemapUrl(URL url) {
+ this(new Options(url));
+ }
+
+ public GoogleImageSitemapUrl(Options options) {
+ super(options);
+ this.images = options.images;
+ }
+
+ public void addImage(Image image) {
+ this.images.add(image);
+ if(this.images.size() > 1000) {
+ throw new RuntimeException("A URL cannot have more than 1000 image tags");
+ }
+ }
+
+ /** Options to configure Google Extension URLs */
+ public static class Options extends AbstractSitemapUrlOptions {
+ private List images;
+
+
+ public Options(URL url) {
+ super(url, GoogleImageSitemapUrl.class);
+ images = new ArrayList();
+ }
+
+ public Options(String url) throws MalformedURLException {
+ super(url, GoogleImageSitemapUrl.class);
+ images = new ArrayList();
+ }
+
+ public Options images(List images) {
+ if(images != null && images.size() > 1000) {
+ throw new RuntimeException("A URL cannot have more than 1000 image tags");
+ }
+ this.images = images;
+ return this;
+ }
+
+ public Options images(Image...images) {
+ if(images.length > 1000) {
+ throw new RuntimeException("A URL cannot have more than 1000 image tags");
+ }
+ return images(Arrays.asList(images));
+
+ }
+ }
+
+ /**Retrieves list of images*/
+ public List getImages() {
+ return this.images;
+ }
+}
diff --git a/src/main/java/com/redfin/sitemapgenerator/Image.java b/src/main/java/com/redfin/sitemapgenerator/Image.java
new file mode 100644
index 0000000..d1773be
--- /dev/null
+++ b/src/main/java/com/redfin/sitemapgenerator/Image.java
@@ -0,0 +1,100 @@
+package com.redfin.sitemapgenerator;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+/**
+ * Represent a single image and image properties for use in extended sitemaps
+ * @see Image sitemaps
+ */
+public class Image {
+ private final URL url;
+ private final String title;
+ private final String caption;
+ private final String geoLocation;
+ private final URL license;
+
+ public Image(String url) throws MalformedURLException {
+ this(new URL(url));
+ }
+
+ public Image(URL url) {
+ this.url = url;
+ this.title = null;
+ this.caption = null;
+ this.geoLocation = null;
+ this.license = null;
+ }
+
+ public Image(URL url, String title, String caption, String geoLocation, String license) throws MalformedURLException {
+ this(url, title, caption, geoLocation, new URL(license));
+ }
+
+ public Image(URL url, String title, String caption, String geoLocation, URL license) {
+ this.url = url;
+ this.title = title;
+ this.caption = caption;
+ this.geoLocation = geoLocation;
+ this.license = license;
+ }
+
+
+ /** Retrieves URL of Image*/
+ public URL getUrl() { return url; }
+
+ /** Retrieves title of image*/
+ public String getTitle() { return title; }
+
+ /** Retrieves captionof image*/
+ public String getCaption() { return caption; }
+
+ /** Retrieves geolocation string of image*/
+ public String getGeoLocation() { return geoLocation; }
+
+ /** Retrieves license string of image*/
+ public URL getLicense() { return license; }
+
+ public static class ImageBuilder {
+ private URL url;
+ private String title;
+ private String caption;
+ private String geoLocation;
+ private URL license;
+
+ public ImageBuilder(String url) throws MalformedURLException {
+ this(new URL(url));
+ }
+
+ public ImageBuilder(URL url) {
+ this.url = url;
+ }
+
+ public ImageBuilder title(String title) {
+ this.title = title;
+ return this;
+ }
+
+ public ImageBuilder caption(String caption) {
+ this.caption = caption;
+ return this;
+ }
+
+ public ImageBuilder geoLocation(String geoLocation) {
+ this.geoLocation = geoLocation;
+ return this;
+ }
+
+ public ImageBuilder license(String license) throws MalformedURLException {
+ return license(new URL(license));
+ }
+
+ public ImageBuilder license(URL license) {
+ this.license = license;
+ return this;
+ }
+
+ public Image build() {
+ return new Image(url, title, caption, geoLocation, license);
+ }
+ }
+}
diff --git a/src/test/java/com/redfin/sitemapgenerator/GoogleImageSitemapUrlTest.java b/src/test/java/com/redfin/sitemapgenerator/GoogleImageSitemapUrlTest.java
new file mode 100644
index 0000000..d6f694f
--- /dev/null
+++ b/src/test/java/com/redfin/sitemapgenerator/GoogleImageSitemapUrlTest.java
@@ -0,0 +1,165 @@
+package com.redfin.sitemapgenerator;
+
+import junit.framework.TestCase;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+public class GoogleImageSitemapUrlTest extends TestCase {
+
+ private static final URL LANDING_URL = newURL("http://www.example.com/index.html");
+ private static final URL CONTENT_URL = newURL("http://www.example.com/index.flv");
+ File dir;
+ GoogleImageSitemapGenerator wsg;
+
+ private static URL newURL(String url) {
+ try {
+ return new URL(url);
+ } catch (MalformedURLException e) {}
+ return null;
+ }
+
+ public void setUp() throws Exception {
+ dir = File.createTempFile(GoogleVideoSitemapUrlTest.class.getSimpleName(), "");
+ dir.delete();
+ dir.mkdir();
+ dir.deleteOnExit();
+ }
+
+ public void tearDown() {
+ wsg = null;
+ for (File file : dir.listFiles()) {
+ file.deleteOnExit();
+ file.delete();
+ }
+ dir.delete();
+ dir = null;
+ }
+
+ public void testSimpleUrl() throws Exception {
+ wsg = new GoogleImageSitemapGenerator("http://www.example.com", dir);
+ GoogleImageSitemapUrl url = new GoogleImageSitemapUrl(LANDING_URL);
+ url.addImage(new Image("http://cdn.example.com/image1.jpg"));
+ url.addImage(new Image("http://cdn.example.com/image2.jpg"));
+ wsg.addUrl(url);
+ String expected = "\n" +
+ "\n" +
+ " \n" +
+ " http://www.example.com/index.html\n" +
+ " \n" +
+ " http://cdn.example.com/image1.jpg\n" +
+ " \n" +
+ " \n" +
+ " http://cdn.example.com/image2.jpg\n" +
+ " \n" +
+ " \n" +
+ "";
+ String sitemap = writeSingleSiteMap(wsg);
+ assertEquals(expected, sitemap);
+ }
+
+ public void testBaseOptions() throws Exception {
+ wsg = new GoogleImageSitemapGenerator("http://www.example.com", dir);
+ GoogleImageSitemapUrl url = new GoogleImageSitemapUrl.Options(LANDING_URL)
+ .images(new Image("http://cdn.example.com/image1.jpg"), new Image("http://cdn.example.com/image2.jpg"))
+ .priority(0.5)
+ .changeFreq(ChangeFreq.WEEKLY)
+ .build();
+ wsg.addUrl(url);
+
+ String expected = "\n" +
+ "\n" +
+ " \n" +
+ " http://www.example.com/index.html\n" +
+ " weekly\n" +
+ " 0.5\n" +
+ " \n" +
+ " http://cdn.example.com/image1.jpg\n" +
+ " \n" +
+ " \n" +
+ " http://cdn.example.com/image2.jpg\n" +
+ " \n" +
+ " \n" +
+ "";
+
+ String sitemap = writeSingleSiteMap(wsg);
+ assertEquals(expected, sitemap);
+ }
+
+ public void testImageOptions() throws Exception {
+ wsg = new GoogleImageSitemapGenerator("http://www.example.com", dir);
+ GoogleImageSitemapUrl url = new GoogleImageSitemapUrl.Options(LANDING_URL)
+ .images(new Image.ImageBuilder("http://cdn.example.com/image1.jpg")
+ .title("image1.jpg")
+ .caption("An image of the number 1")
+ .geoLocation("Pyongyang, North Korea")
+ .license("http://cdn.example.com/licenses/imagelicense.txt")
+ .build(),
+ new Image.ImageBuilder("http://cdn.example.com/image2.jpg")
+ .title("image2.jpg")
+ .caption("An image of the number 2")
+ .geoLocation("Pyongyang, North Korea")
+ .license("http://cdn.example.com/licenses/imagelicense.txt")
+ .build())
+ .priority(0.5)
+ .changeFreq(ChangeFreq.WEEKLY)
+ .build();
+ wsg.addUrl(url);
+
+ String expected = "\n" +
+ "\n" +
+ " \n" +
+ " http://www.example.com/index.html\n" +
+ " weekly\n" +
+ " 0.5\n" +
+ " \n" +
+ " http://cdn.example.com/image1.jpg\n" +
+ " An image of the number 1\n" +
+ " image1.jpg\n" +
+ " Pyongyang, North Korea\n" +
+ " http://cdn.example.com/licenses/imagelicense.txt\n" +
+ " \n" +
+ " \n" +
+ " http://cdn.example.com/image2.jpg\n" +
+ " An image of the number 2\n" +
+ " image2.jpg\n" +
+ " Pyongyang, North Korea\n" +
+ " http://cdn.example.com/licenses/imagelicense.txt\n" +
+ " \n" +
+ " \n" +
+ "";
+
+ String sitemap = writeSingleSiteMap(wsg);
+ assertEquals(expected, sitemap);
+ }
+
+ public void testTooManyImages() throws Exception {
+ wsg = new GoogleImageSitemapGenerator("http://www.example.com", dir);
+ List images = new ArrayList();
+ for(int i = 0; i <= 1000; i++) {
+ images.add(new Image("http://cdn.example.com/image" + i + ".jpg"));
+ }
+ try {
+ GoogleImageSitemapUrl url = new GoogleImageSitemapUrl.Options(LANDING_URL)
+ .images(images)
+ .priority(0.5)
+ .changeFreq(ChangeFreq.WEEKLY)
+ .build();
+ fail("Too many images allowed");
+ } catch (RuntimeException r) {}
+ }
+
+
+
+ private String writeSingleSiteMap(GoogleImageSitemapGenerator wsg) {
+ List 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));
+ }
+
+}