You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by di...@apache.org on 2021/12/30 18:20:35 UTC

[sling-org-apache-sling-sitemap] 01/01: SLING-10562: add google news extension

This is an automated email from the ASF dual-hosted git repository.

diru pushed a commit to branch issue/SLING-10562
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-sitemap.git

commit c68d96a563b2150102937f4a575c7da94b26236c
Author: Dirk Rudolph <di...@apache.org>
AuthorDate: Thu Dec 30 19:20:21 2021 +0100

    SLING-10562: add google news extension
---
 pom.xml                                            |   2 +-
 .../builder/extensions/GoogleNewsExtension.java    |  98 +++++++++++
 .../extensions/GoogleNewsExtensionProvider.java    | 184 +++++++++++++++++++++
 .../sitemap/impl/builder/AbstractBuilderTest.java  |   3 +-
 .../extensions/AlternateLanguageExtensionTest.java |   1 +
 .../extensions/GoogleNewsExtensionTest.java        | 158 ++++++++++++++++++
 src/test/resources/sitemap-news-0.9.xsd            | 159 ++++++++++++++++++
 7 files changed, 603 insertions(+), 2 deletions(-)

diff --git a/pom.xml b/pom.xml
index f36c88c..7b6fee6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -60,13 +60,13 @@
                     <excludes combine.children="append">
                         <!-- Obtained from http://sitemaps.org/ -->
                         <exclude>src/test/resources/sitemap-0.9.xsd</exclude>
-                        <!-- Obtained from http://sitemaps.org/ -->
                         <exclude>src/test/resources/siteindex-0.9.xsd</exclude>
                         <!-- Obtained from http://w3.org/ -->
                         <exclude>src/test/resources/xhtml1-strict.xsd</exclude>
                         <exclude>src/test/resources/xml.xsd</exclude>
                         <!-- Obtained from https://www.google.com/ -->
                         <exclude>src/test/resources/sitemap-image-1.1.xsd</exclude>
+                        <exclude>src/test/resources/sitemap-news-0.9.xsd</exclude>
                     </excludes>
                 </configuration>
             </plugin>
diff --git a/src/main/java/org/apache/sling/sitemap/builder/extensions/GoogleNewsExtension.java b/src/main/java/org/apache/sling/sitemap/builder/extensions/GoogleNewsExtension.java
new file mode 100644
index 0000000..46dd5f2
--- /dev/null
+++ b/src/main/java/org/apache/sling/sitemap/builder/extensions/GoogleNewsExtension.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.sitemap.builder.extensions;
+
+import org.apache.sling.sitemap.builder.Extension;
+import org.jetbrains.annotations.NotNull;
+import org.osgi.annotation.versioning.ProviderType;
+
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.OffsetDateTime;
+import java.util.Locale;
+
+/**
+ * An extension to add news metadata to an {@link org.apache.sling.sitemap.builder.Url}.
+ *
+ * @see <a href="https://developers.google.com/search/docs/advanced/sitemaps/news-sitemap">Google News sitemaps</a>
+ */
+@ProviderType
+public interface GoogleNewsExtension extends Extension {
+
+    enum AccessRestriction {
+        SUBSCRIPTION("Subscription"),
+        REGISTRATION("Registration");
+
+        private final String value;
+
+        AccessRestriction(String value) {
+            this.value = value;
+        }
+
+        public String getValue() {
+            return value;
+        }
+    }
+
+    enum Genre {
+        PRESS_RELEASE("PressRelease"),
+        SATIRE("Satire"),
+        BLOG("Blog"),
+        OP_ED("OpEd"),
+        OPINION("Opinion"),
+        USER_GENERATED("UserGenerated");
+
+        private final String value;
+
+        Genre(String value) {
+            this.value = value;
+        }
+
+        public String getValue() {
+            return value;
+        }
+    }
+
+    @NotNull
+    GoogleNewsExtension setPublicationName(@NotNull String name);
+
+    @NotNull
+    GoogleNewsExtension setPublicationLanguage(@NotNull Locale locale);
+
+    @NotNull
+    GoogleNewsExtension setPublicationDate(@NotNull OffsetDateTime date);
+
+    @NotNull
+    GoogleNewsExtension setPublicationDate(@NotNull LocalDate date);
+
+    @NotNull
+    GoogleNewsExtension setTitle(@NotNull String title);
+
+    @NotNull
+    GoogleNewsExtension setAccessRestriction(AccessRestriction accessRestriction);
+
+    @NotNull
+    GoogleNewsExtension setGenres(Genre... genres);
+
+    @NotNull
+    GoogleNewsExtension setKeywords(String... keywords);
+
+    @NotNull
+    GoogleNewsExtension setStockTickers(String ...stockTickers);
+}
diff --git a/src/main/java/org/apache/sling/sitemap/impl/builder/extensions/GoogleNewsExtensionProvider.java b/src/main/java/org/apache/sling/sitemap/impl/builder/extensions/GoogleNewsExtensionProvider.java
new file mode 100644
index 0000000..14239a5
--- /dev/null
+++ b/src/main/java/org/apache/sling/sitemap/impl/builder/extensions/GoogleNewsExtensionProvider.java
@@ -0,0 +1,184 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.sitemap.impl.builder.extensions;
+
+import org.apache.sling.sitemap.builder.extensions.GoogleNewsExtension;
+import org.apache.sling.sitemap.spi.builder.AbstractExtension;
+import org.apache.sling.sitemap.spi.builder.SitemapExtensionProvider;
+import org.jetbrains.annotations.NotNull;
+import org.osgi.service.component.annotations.Component;
+
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.OffsetDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.UnsupportedTemporalTypeException;
+import java.util.Arrays;
+import java.util.Locale;
+import java.util.stream.Collectors;
+
+@Component(
+        property = {
+                SitemapExtensionProvider.PROPERTY_INTERFACE + "=org.apache.sling.sitemap.builder.extensions.GoogleNewsExtension",
+                SitemapExtensionProvider.PROPERTY_PREFIX + "=news",
+                SitemapExtensionProvider.PROPERTY_NAMESPACE + "=http://www.google.com/schemas/sitemap-news/0.9",
+                SitemapExtensionProvider.PROPERTY_LOCAL_NAME + "=news"
+        }
+)
+public class GoogleNewsExtensionProvider implements SitemapExtensionProvider {
+
+    @Override
+    public AbstractExtension newInstance() {
+        return new ExtensionImpl();
+    }
+
+    private static class ExtensionImpl extends AbstractExtension implements GoogleNewsExtension {
+
+        private String publicationName;
+        private String publicationLanguage;
+        private String publicationDate;
+        private String title;
+        private String accessRestriction;
+        private String genres;
+        private String keywords;
+        private String stockTickers;
+
+        @Override
+        @NotNull
+        public GoogleNewsExtension setPublicationName(@NotNull String name) {
+            this.publicationName = name;
+            return this;
+        }
+
+        @Override
+        @NotNull
+        public GoogleNewsExtension setPublicationLanguage(@NotNull Locale locale) {
+            this.publicationLanguage = locale.toLanguageTag().toLowerCase(Locale.ROOT);
+            return this;
+        }
+
+        @Override
+        @NotNull
+        public GoogleNewsExtension setPublicationDate(@NotNull OffsetDateTime date) {
+            this.publicationDate = DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(date);
+            return this;
+        }
+
+        @Override
+        @NotNull
+        public GoogleNewsExtension setPublicationDate(@NotNull LocalDate date) {
+            this.publicationDate = DateTimeFormatter.ISO_LOCAL_DATE.format(date);
+            return this;
+        }
+
+        @Override
+        @NotNull
+        public GoogleNewsExtension setTitle(@NotNull String title) {
+            this.title = title;
+            return this;
+        }
+
+        @Override
+        @NotNull
+        public GoogleNewsExtension setAccessRestriction(AccessRestriction accessRestriction) {
+            this.accessRestriction = accessRestriction != null ? accessRestriction.getValue() : null;
+            return this;
+        }
+
+        @Override
+        @NotNull
+        public GoogleNewsExtension setGenres(Genre... genres) {
+            this.genres = genres.length > 0
+                    ? Arrays.stream(genres).map(Genre::getValue).collect(Collectors.joining(","))
+                    : null;
+            return this;
+        }
+
+        @Override
+        @NotNull
+        public GoogleNewsExtension setKeywords(String... keywords) {
+            this.keywords = keywords.length > 0
+                    ? String.join(",", keywords)
+                    : null;
+            return this;
+        }
+
+        @Override
+        @NotNull
+        public GoogleNewsExtension setStockTickers(String... stockTickers) {
+            this.stockTickers = stockTickers.length > 0
+                    ? String.join(",", stockTickers)
+                    : null;
+            return this;
+        }
+
+        @Override
+        public void writeTo(@NotNull XMLStreamWriter writer) throws XMLStreamException {
+            writer.writeStartElement("publication");
+            writer.writeStartElement("name");
+            writer.writeCharacters(required(publicationName,"publication name missing"));
+            writer.writeEndElement();
+            writer.writeStartElement("language");
+            writer.writeCharacters(required(publicationLanguage, "publication language missing"));
+            writer.writeEndElement();
+            writer.writeEndElement();
+
+            if (accessRestriction != null) {
+                writer.writeStartElement("access");
+                writer.writeCharacters(accessRestriction);
+                writer.writeEndElement();
+            }
+
+            if (genres != null) {
+                writer.writeStartElement("genres");
+                writer.writeCharacters(genres);
+                writer.writeEndElement();
+            }
+
+            writer.writeStartElement("publication_date");
+            writer.writeCharacters(required(publicationDate, "publication date missing"));
+            writer.writeEndElement();
+
+            writer.writeStartElement("title");
+            writer.writeCharacters(required(title,"title missing"));
+            writer.writeEndElement();
+
+            if (keywords != null) {
+                writer.writeStartElement("keywords");
+                writer.writeCharacters(keywords);
+                writer.writeEndElement();
+            }
+
+            if (stockTickers != null) {
+                writer.writeStartElement("stock_tickers");
+                writer.writeCharacters(stockTickers);
+                writer.writeEndElement();
+            }
+        }
+
+        private static String required(String object, String message) throws XMLStreamException {
+            if (object == null) {
+                throw new XMLStreamException(message);
+            }
+            return object;
+        }
+    }
+}
diff --git a/src/test/java/org/apache/sling/sitemap/impl/builder/AbstractBuilderTest.java b/src/test/java/org/apache/sling/sitemap/impl/builder/AbstractBuilderTest.java
index e1ceeca..2da65a7 100644
--- a/src/test/java/org/apache/sling/sitemap/impl/builder/AbstractBuilderTest.java
+++ b/src/test/java/org/apache/sling/sitemap/impl/builder/AbstractBuilderTest.java
@@ -43,9 +43,10 @@ public abstract class AbstractBuilderTest {
     private static final URL SITEMAP_XSD = AbstractBuilderTest.class.getClassLoader().getResource("sitemap-0.9.xsd");
     private static final URL SITEMAP_INDEX_XSD = AbstractBuilderTest.class.getClassLoader().getResource("siteindex-0.9.xsd");
     private static final URL SITEMAP_IMAGE_XSD = AbstractBuilderTest.class.getClassLoader().getResource("sitemap-image-1.1.xsd");
+    private static final URL SITEMAP_NEWS_XSD = AbstractBuilderTest.class.getClassLoader().getResource("sitemap-news-0.9.xsd");
 
     protected void assertSitemap(String expected, String given) {
-        assertEqualsAndValid(expected, given, XML_XSD, XHTML_XSD, SITEMAP_XSD, SITEMAP_IMAGE_XSD);
+        assertEqualsAndValid(expected, given, XML_XSD, XHTML_XSD, SITEMAP_XSD, SITEMAP_IMAGE_XSD, SITEMAP_NEWS_XSD);
     }
 
     protected void assertSitemapIndex(String expected, String given) {
diff --git a/src/test/java/org/apache/sling/sitemap/impl/builder/extensions/AlternateLanguageExtensionTest.java b/src/test/java/org/apache/sling/sitemap/impl/builder/extensions/AlternateLanguageExtensionTest.java
index f4d1dc9..e25c08e 100644
--- a/src/test/java/org/apache/sling/sitemap/impl/builder/extensions/AlternateLanguageExtensionTest.java
+++ b/src/test/java/org/apache/sling/sitemap/impl/builder/extensions/AlternateLanguageExtensionTest.java
@@ -88,6 +88,7 @@ class AlternateLanguageExtensionTest extends AbstractBuilderTest {
 
         // when
         Url url = sitemap.addUrl("http://example.ch/de.html");
+        url.addExtension(AlternateLanguageExtension.class);
         url.addExtension(AlternateLanguageExtension.class)
             .setLocale(Locale.forLanguageTag("fr-ch"));
         url.addExtension(AlternateLanguageExtension.class)
diff --git a/src/test/java/org/apache/sling/sitemap/impl/builder/extensions/GoogleNewsExtensionTest.java b/src/test/java/org/apache/sling/sitemap/impl/builder/extensions/GoogleNewsExtensionTest.java
new file mode 100644
index 0000000..bf0bf6a
--- /dev/null
+++ b/src/test/java/org/apache/sling/sitemap/impl/builder/extensions/GoogleNewsExtensionTest.java
@@ -0,0 +1,158 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.sitemap.impl.builder.extensions;
+
+import org.apache.sling.sitemap.SitemapException;
+import org.apache.sling.sitemap.builder.Url;
+import org.apache.sling.sitemap.builder.extensions.GoogleImageExtension;
+import org.apache.sling.sitemap.builder.extensions.GoogleNewsExtension;
+import org.apache.sling.sitemap.impl.builder.AbstractBuilderTest;
+import org.apache.sling.sitemap.impl.builder.SitemapImpl;
+import org.apache.sling.testing.mock.sling.junit5.SlingContext;
+import org.apache.sling.testing.mock.sling.junit5.SlingContextExtension;
+import org.junit.Ignore;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.OffsetDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Calendar;
+import java.util.Locale;
+
+@ExtendWith({SlingContextExtension.class})
+class GoogleNewsExtensionTest extends AbstractBuilderTest {
+
+    final SlingContext context = new SlingContext();
+
+    private ExtensionProviderManager extensionProviderManager;
+
+    @BeforeEach
+    void setup() {
+        context.registerInjectActivateService(new GoogleNewsExtensionProvider());
+        extensionProviderManager = context.registerInjectActivateService(new ExtensionProviderManager());
+    }
+
+    @Test
+    void testGoogleNewsCombinations() throws SitemapException, IOException {
+        // given
+        StringWriter writer = new StringWriter();
+        SitemapImpl sitemap = new SitemapImpl(writer, extensionProviderManager);
+
+        // when
+        Url url = sitemap.addUrl("http://example.ch/de.html");
+        url.addExtension(GoogleNewsExtension.class)
+                .setPublicationName("News 1")
+                .setPublicationLanguage(Locale.ENGLISH)
+                .setPublicationDate(LocalDate.parse("2021-12-30"))
+                .setTitle("News Title 1");
+        url.addExtension(GoogleNewsExtension.class)
+                .setPublicationName("News 2")
+                .setPublicationLanguage(Locale.GERMAN)
+                .setPublicationDate(OffsetDateTime.parse("2021-12-30T12:34:56.000+01:00"))
+                .setTitle("News Title 2");
+        url.addExtension(GoogleNewsExtension.class)
+                .setPublicationName("News 3")
+                .setPublicationLanguage(new Locale("zh", "tw"))
+                .setPublicationDate(OffsetDateTime.parse("2021-12-30T12:34:56.000+02:00"))
+                .setTitle("News Title 3")
+                .setGenres(GoogleNewsExtension.Genre.PRESS_RELEASE, GoogleNewsExtension.Genre.SATIRE)
+                .setAccessRestriction(GoogleNewsExtension.AccessRestriction.SUBSCRIPTION)
+                .setKeywords("foo", "bar")
+                .setStockTickers("NASDAQ:FOO");
+        sitemap.close();
+
+        // then
+
+        assertSitemap(
+                AbstractBuilderTest.XML_HEADER + "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" " +
+                        "xmlns:news=\"http://www.google.com/schemas/sitemap-news/0.9\">"
+                        + "<url>"
+                        + "<loc>http://example.ch/de.html</loc>"
+                        + "<news:news>"
+                        + "<news:publication>"
+                        + "<news:name>News 1</news:name>"
+                        + "<news:language>en</news:language>"
+                        + "</news:publication>"
+                        + "<news:publication_date>2021-12-30</news:publication_date>"
+                        + "<news:title>News Title 1</news:title>"
+                        + "</news:news>"
+                        + "<news:news>"
+                        + "<news:publication>"
+                        + "<news:name>News 2</news:name>"
+                        + "<news:language>de</news:language>"
+                        + "</news:publication>"
+                        + "<news:publication_date>2021-12-30T12:34:56+01:00</news:publication_date>"
+                        + "<news:title>News Title 2</news:title>"
+                        + "</news:news>"
+                        + "<news:news>"
+                        + "<news:publication>"
+                        + "<news:name>News 3</news:name>"
+                        + "<news:language>zh-tw</news:language>"
+                        + "</news:publication>"
+                        + "<news:access>Subscription</news:access>"
+                        + "<news:genres>PressRelease,Satire</news:genres>"
+                        + "<news:publication_date>2021-12-30T12:34:56+02:00</news:publication_date>"
+                        + "<news:title>News Title 3</news:title>"
+                        + "<news:keywords>foo,bar</news:keywords>"
+                        + "<news:stock_tickers>NASDAQ:FOO</news:stock_tickers>"
+                        + "</news:news>"
+                        + "</url>"
+                        + "</urlset>",
+                writer.toString()
+        );
+    }
+
+    @Test
+    void testNothingWrittenWhenExtensionMissesMandatoryProperties() throws SitemapException, IOException {
+        // given
+        StringWriter writer = new StringWriter();
+        SitemapImpl sitemap = new SitemapImpl(writer, extensionProviderManager);
+
+        // when
+        Url url = sitemap.addUrl("http://example.ch/de.html");
+        url.addExtension(GoogleNewsExtension.class);
+        url.addExtension(GoogleNewsExtension.class)
+                .setPublicationName("name");
+        url.addExtension(GoogleNewsExtension.class)
+                .setPublicationName("name")
+                .setPublicationLanguage(Locale.ENGLISH);
+        url.addExtension(GoogleNewsExtension.class)
+                .setPublicationName("name")
+                .setPublicationLanguage(Locale.ENGLISH)
+                .setPublicationDate(LocalDate.now());
+        sitemap.close();
+
+        // then
+        assertSitemap(
+                AbstractBuilderTest.XML_HEADER + "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" " +
+                        "xmlns:news=\"http://www.google.com/schemas/sitemap-news/0.9\">"
+                        + "<url>"
+                        + "<loc>http://example.ch/de.html</loc>"
+                        + "</url>"
+                        + "</urlset>",
+                writer.toString()
+        );
+    }
+
+}
diff --git a/src/test/resources/sitemap-news-0.9.xsd b/src/test/resources/sitemap-news-0.9.xsd
new file mode 100644
index 0000000..7e3e7eb
--- /dev/null
+++ b/src/test/resources/sitemap-news-0.9.xsd
@@ -0,0 +1,159 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xsd:schema
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+    targetNamespace="http://www.google.com/schemas/sitemap-news/0.9"
+    xmlns="http://www.google.com/schemas/sitemap-news/0.9"
+    elementFormDefault="qualified">
+
+<xsd:annotation>
+  <xsd:documentation>
+    XML Schema for the News Sitemap extension.  This schema defines the
+    News-specific elements only; the core Sitemap elements are defined
+    separately.
+
+    Help Center documentation for the News Sitemap extension:
+
+      http://www.google.com/support/news_pub/bin/topic.py?topic=11666
+
+    Copyright 2010 Google Inc. All Rights Reserved.
+  </xsd:documentation>
+</xsd:annotation>
+
+<xsd:element name="news">
+  <xsd:complexType>
+    <xsd:sequence>
+      <xsd:element name="publication">
+        <xsd:annotation>
+          <xsd:documentation>
+            The publication in which the article appears.  Required.
+          </xsd:documentation>
+        </xsd:annotation>
+        <xsd:complexType>
+          <xsd:sequence>
+            <xsd:element name="name" type="xsd:string">
+              <xsd:annotation>
+                <xsd:documentation>
+                  Name of the news publication. It must exactly match
+                  the name as it appears on your articles in news.google.com,
+                  omitting any trailing parentheticals.
+                  For example, if the name appears in Google News as
+                  "The Example Times (subscription)", you should use
+                  "The Example Times".  Required.
+                </xsd:documentation>
+              </xsd:annotation>
+            </xsd:element>
+            <xsd:element name="language">
+              <xsd:annotation>
+                <xsd:documentation>
+                  Language of the publication.  It should be an
+                  ISO 639 Language Code (either 2 or 3 letters); see:
+                    http://www.loc.gov/standards/iso639-2/php/code_list.php
+                  Exception: For Chinese, please use zh-cn for Simplified
+                  Chinese or zh-tw for Traditional Chinese.  Required.
+                </xsd:documentation>
+              </xsd:annotation>
+              <xsd:simpleType>
+                <xsd:restriction base="xsd:string">
+                  <xsd:pattern value="zh-cn|zh-tw|([a-z]{2,3})"/>
+                </xsd:restriction>
+              </xsd:simpleType>
+            </xsd:element>
+          </xsd:sequence>
+        </xsd:complexType>
+      </xsd:element>
+      <xsd:element name="access" minOccurs="0">
+        <xsd:annotation>
+          <xsd:documentation>
+            Accessibility of the article.  Required if access is not open,
+            otherwise this tag should be omitted.
+          </xsd:documentation>
+        </xsd:annotation>
+        <xsd:simpleType>
+          <xsd:restriction base="xsd:string">
+            <xsd:enumeration value="Subscription"/>
+            <xsd:enumeration value="Registration"/>
+          </xsd:restriction>
+        </xsd:simpleType>
+      </xsd:element>
+      <xsd:element name="genres" minOccurs="0">
+        <xsd:annotation>
+          <xsd:documentation>
+            A comma-separated list of properties characterizing the content
+            of the article, such as "PressRelease" or "UserGenerated".
+            For a list of possible values, see:
+              http://www.google.com/support/news_pub/bin/answer.py?answer=93992
+            Required if any genres apply to the article, otherwise this tag
+            should be omitted.
+          </xsd:documentation>
+        </xsd:annotation>
+        <xsd:simpleType>
+          <xsd:restriction base="xsd:string">
+            <xsd:pattern value="(PressRelease|Satire|Blog|OpEd|Opinion|UserGenerated)(, *(PressRelease|Satire|Blog|OpEd|Opinion|UserGenerated))*"/>
+          </xsd:restriction>
+        </xsd:simpleType>
+      </xsd:element>
+      <xsd:element name="publication_date">
+        <xsd:annotation>
+          <xsd:documentation>
+            Article publication date in W3C format, specifying the complete
+            date (YYYY-MM-DD) with optional timestamp.  See:
+              http://www.w3.org/TR/NOTE-datetime
+            Please ensure that you give the original date and time at which
+            the article was published on your site; do not give the time
+            at which the article was added to your Sitemap.  Required.
+          </xsd:documentation>
+        </xsd:annotation>
+        <xsd:simpleType>
+          <xsd:union>
+            <xsd:simpleType>
+              <xsd:restriction base="xsd:date"/>
+            </xsd:simpleType>
+            <xsd:simpleType>
+              <xsd:restriction base="xsd:dateTime"/>
+            </xsd:simpleType>
+          </xsd:union>
+        </xsd:simpleType>
+      </xsd:element>
+      <xsd:element name="title" type="xsd:string">
+        <xsd:annotation>
+          <xsd:documentation>
+            Title of the news article.  Required.
+            Note: The title may be truncated for space reasons when shown
+            on Google News.
+          </xsd:documentation>
+        </xsd:annotation>
+      </xsd:element>
+      <xsd:element name="keywords" type="xsd:string" minOccurs="0">
+        <xsd:annotation>
+          <xsd:documentation>
+            Comma-separated list of keywords describing the topic of
+            the article.  Keywords may be drawn from, but are not limited to,
+            the list of existing Google News keywords; see:
+              http://www.google.com/support/news_pub/bin/answer.py?answer=116037
+            Optional.
+          </xsd:documentation>
+        </xsd:annotation>
+      </xsd:element>
+      <xsd:element name="stock_tickers" minOccurs="0">
+        <xsd:annotation>
+          <xsd:documentation>
+            Comma-separated list of up to 5 stock tickers of the companies,
+            mutual funds, or other financial entities that are the main subject
+            of the article.  Relevant primarily for business articles.
+            Each ticker must be prefixed by the name of its stock exchange,
+            and must match its entry in Google Finance.
+            For example, "NASDAQ:AMAT" (but not "NASD:AMAT"),
+            or "BOM:500325" (but not "BOM:RIL").  Optional.
+          </xsd:documentation>
+        </xsd:annotation>
+        <xsd:simpleType>
+          <xsd:restriction base="xsd:string">
+            <xsd:pattern value="(\w+:\w+(, *\w+:\w+){0,4})?"/>
+          </xsd:restriction>
+        </xsd:simpleType>
+      </xsd:element>
+    </xsd:sequence>
+  </xsd:complexType>
+</xsd:element>
+
+</xsd:schema>