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:34 UTC

[sling-org-apache-sling-sitemap] branch issue/SLING-10562 created (now c68d96a)

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

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


      at c68d96a  SLING-10562: add google news extension

This branch includes the following new commits:

     new c68d96a  SLING-10562: add google news extension

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


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

Posted by di...@apache.org.
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>