diff --git a/org.springframework.testsuite/ivy.xml b/org.springframework.testsuite/ivy.xml
index 831f887f8da..0428624b0a8 100644
--- a/org.springframework.testsuite/ivy.xml
+++ b/org.springframework.testsuite/ivy.xml
@@ -24,6 +24,7 @@
+
@@ -74,7 +75,8 @@
-
+
+
diff --git a/org.springframework.testsuite/src/test/java/org/springframework/web/servlet/view/feed/AtomFeedViewTest.java b/org.springframework.testsuite/src/test/java/org/springframework/web/servlet/view/feed/AtomFeedViewTest.java
new file mode 100644
index 00000000000..ed4c14271ff
--- /dev/null
+++ b/org.springframework.testsuite/src/test/java/org/springframework/web/servlet/view/feed/AtomFeedViewTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright ${YEAR} the original author or authors.
+ *
+ * Licensed 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.springframework.web.servlet.view.feed;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import com.sun.syndication.feed.atom.Content;
+import com.sun.syndication.feed.atom.Entry;
+import com.sun.syndication.feed.atom.Feed;
+import static org.custommonkey.xmlunit.XMLAssert.assertXMLEqual;
+import static org.custommonkey.xmlunit.XMLUnit.setIgnoreWhitespace;
+import static org.junit.Assert.assertEquals;
+import org.junit.Before;
+import org.junit.Test;
+
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+
+public class AtomFeedViewTest {
+
+ private AbstractAtomFeedView view;
+
+ @Before
+ public void createView() throws Exception {
+ view = new MyAtomFeedView();
+ setIgnoreWhitespace(true);
+ }
+
+ @Test
+ public void render() throws Exception {
+ MockHttpServletRequest request = new MockHttpServletRequest();
+ MockHttpServletResponse response = new MockHttpServletResponse();
+
+ Map model = new HashMap();
+ model.put("1", "This is entry 1");
+ model.put("2", "This is entry 2");
+
+ view.render(model, request, response);
+ assertEquals("Invalid content-type", "application/atom+xml", response.getContentType());
+ String expected = "" + "Test Feed" +
+ "2This is entry 2" +
+ "1This is entry 1" + "";
+ assertXMLEqual(expected, response.getContentAsString());
+ }
+
+ private static class MyAtomFeedView extends AbstractAtomFeedView {
+
+ @Override
+ protected void buildFeedMetadata(Map model, Feed feed, HttpServletRequest request) {
+ feed.setTitle("Test Feed");
+ }
+
+ @Override
+ protected List buildFeedEntries(Map model, HttpServletRequest request, HttpServletResponse response)
+ throws Exception {
+ List entries = new ArrayList();
+ for (Iterator iterator = model.keySet().iterator(); iterator.hasNext();) {
+ String name = (String) iterator.next();
+ Entry entry = new Entry();
+ entry.setTitle(name);
+ Content content = new Content();
+ content.setValue((String) model.get(name));
+ entry.setSummary(content);
+ entries.add(entry);
+ }
+ return entries;
+ }
+ }
+}
\ No newline at end of file
diff --git a/org.springframework.testsuite/src/test/java/org/springframework/web/servlet/view/feed/RssFeedViewTest.java b/org.springframework.testsuite/src/test/java/org/springframework/web/servlet/view/feed/RssFeedViewTest.java
new file mode 100644
index 00000000000..d5c33d2b5c9
--- /dev/null
+++ b/org.springframework.testsuite/src/test/java/org/springframework/web/servlet/view/feed/RssFeedViewTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright ${YEAR} the original author or authors.
+ *
+ * Licensed 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.springframework.web.servlet.view.feed;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import com.sun.syndication.feed.rss.Channel;
+import com.sun.syndication.feed.rss.Description;
+import com.sun.syndication.feed.rss.Item;
+import static org.custommonkey.xmlunit.XMLAssert.assertXMLEqual;
+import static org.custommonkey.xmlunit.XMLUnit.setIgnoreWhitespace;
+import static org.junit.Assert.assertEquals;
+import org.junit.Before;
+import org.junit.Test;
+
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+
+public class RssFeedViewTest {
+
+ private AbstractRssFeedView view;
+
+ @Before
+ public void createView() throws Exception {
+ view = new MyRssFeedView();
+ setIgnoreWhitespace(true);
+
+ }
+
+ @Test
+ public void render() throws Exception {
+ MockHttpServletRequest request = new MockHttpServletRequest();
+ MockHttpServletResponse response = new MockHttpServletResponse();
+
+ Map model = new HashMap();
+ model.put("1", "This is entry 1");
+ model.put("2", "This is entry 2");
+
+ view.render(model, request, response);
+ assertEquals("Invalid content-type", "application/rss+xml", response.getContentType());
+ String expected = "" +
+ "Test Feedhttp://example.comTest feed description" +
+ "2This is entry 2" +
+ "1This is entry 1" + "";
+ assertXMLEqual(expected, response.getContentAsString());
+ }
+
+ private static class MyRssFeedView extends AbstractRssFeedView {
+
+ @Override
+ protected void buildFeedMetadata(Map model, Channel channel, HttpServletRequest request) {
+ channel.setTitle("Test Feed");
+ channel.setDescription("Test feed description");
+ channel.setLink("http://example.com");
+ }
+
+ @Override
+ protected List buildFeedItems(Map model, HttpServletRequest request, HttpServletResponse response)
+ throws Exception {
+ List items = new ArrayList();
+ for (Iterator iterator = model.keySet().iterator(); iterator.hasNext();) {
+ String name = (String) iterator.next();
+ Item item = new Item();
+ item.setTitle(name);
+ Description description = new Description();
+ description.setValue((String) model.get(name));
+ item.setDescription(description);
+ items.add(item);
+ }
+ return items;
+ }
+ }
+}
\ No newline at end of file
diff --git a/org.springframework.web.servlet/ivy.xml b/org.springframework.web.servlet/ivy.xml
index a3392a65c09..730bed8d66e 100644
--- a/org.springframework.web.servlet/ivy.xml
+++ b/org.springframework.web.servlet/ivy.xml
@@ -13,6 +13,7 @@
+
@@ -28,6 +29,7 @@
+
diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/feed/AbstractAtomFeedView.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/feed/AbstractAtomFeedView.java
new file mode 100644
index 00000000000..659f3251fb6
--- /dev/null
+++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/feed/AbstractAtomFeedView.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2008 the original author or authors.
+ *
+ * Licensed 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.springframework.web.servlet.view.feed;
+
+import java.util.List;
+import java.util.Map;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import com.sun.syndication.feed.WireFeed;
+import com.sun.syndication.feed.atom.Entry;
+import com.sun.syndication.feed.atom.Feed;
+
+/**
+ * Abstract superclass for Atom Feed views, using java.net's ROME package.
+ * Application-specific view classes will extend this class. The view will be held in the subclass itself, not in a
+ * template.
+ *
+ * Main entry points are the {@link #buildFeedMetadata(Map, WireFeed, HttpServletRequest)} and
+ * {@link #buildFeedEntries(Map, HttpServletRequest, HttpServletResponse)}.
+ *
+ * @author Jettro Coenradie
+ * @author Sergio Bossa
+ * @author Arjen Poutsma
+ * @since 3.0
+ * @see #buildFeedMetadata(Map, WireFeed, HttpServletRequest)
+ * @see #buildFeedEntries(Map, HttpServletRequest, HttpServletResponse)
+ */
+public abstract class AbstractAtomFeedView extends AbstractFeedView {
+
+ public static final String DEFAULT_FEED_TYPE = "atom_1.0";
+
+ private String feedType = DEFAULT_FEED_TYPE;
+
+ /** Sets the appropriate content type: "application/atom+xml". */
+ public AbstractAtomFeedView() {
+ setContentType("application/atom+xml");
+ }
+
+ /**
+ * Sets the Rome feed type to use.
+ *
+ * Defaults to Atom 1.0.
+ *
+ * @see Feed#setFeedType(String)
+ * @see #DEFAULT_FEED_TYPE
+ */
+ public void setFeedType(String feedType) {
+ this.feedType = feedType;
+ }
+
+ /**
+ * Create a new feed to hold the entries.
+ *
+ * By default returns an Atom 1.0 feed, but the subclass can specify any Feed.
+ *
+ * @return the newly created Feed instance
+ * @see #setFeedType(String)
+ * @see com.sun.syndication.feed.atom.Feed#Feed(String)
+ */
+ @Override
+ protected Feed newFeed() {
+ return new Feed(feedType);
+ }
+
+ /** Invokes {@link #buildFeedEntries(Map, HttpServletRequest, HttpServletResponse)} to get a list of feed entries. */
+ @Override
+ protected final void buildFeedEntries(Map model,
+ Feed feed,
+ HttpServletRequest request,
+ HttpServletResponse response) throws Exception {
+ List entries = buildFeedEntries(model, request, response);
+ feed.setEntries(entries);
+ }
+
+ /**
+ * Subclasses must implement this method to build feed entries, given the model.
+ *
+ * Note that the passed-in HTTP response is just supposed to be used for setting cookies or other HTTP headers. The
+ * built feed itself will automatically get written to the response after this method returns.
+ *
+ * @param model the model Map
+ * @param request in case we need locale etc. Shouldn't look at attributes.
+ * @param response in case we need to set cookies. Shouldn't write to it.
+ * @return the feed entries to be added to the feed
+ * @throws Exception any exception that occured during document building
+ * @see Entry
+ */
+ protected abstract List buildFeedEntries(Map model, HttpServletRequest request, HttpServletResponse response)
+ throws Exception;
+}
diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/feed/AbstractFeedView.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/feed/AbstractFeedView.java
new file mode 100644
index 00000000000..4f754ccc8f5
--- /dev/null
+++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/feed/AbstractFeedView.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2008 the original author or authors.
+ *
+ * Licensed 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.springframework.web.servlet.view.feed;
+
+import java.io.OutputStreamWriter;
+import java.util.Map;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import com.sun.syndication.feed.WireFeed;
+import com.sun.syndication.io.WireFeedOutput;
+
+import org.springframework.util.StringUtils;
+import org.springframework.web.servlet.view.AbstractView;
+
+/**
+ * Abstract base class for Atom and RSS Feed views, using java.net's ROME
+ * package. TypApplication-specific view classes will typically extends either {@link AbstractRssFeedView} or {@link
+ * AbstractAtomFeedView}, not this class.
+ *
+ * @author Jettro Coenradie
+ * @author Sergio Bossa
+ * @author Arjen Poutsma
+ * @see AbstractRssFeedView
+ * @see AbstractAtomFeedView
+ * @since 3.0
+ */
+public abstract class AbstractFeedView extends AbstractView {
+
+ @Override
+ protected final void renderMergedOutputModel(Map model, HttpServletRequest request, HttpServletResponse response)
+ throws Exception {
+ T wireFeed = newFeed();
+ buildFeedMetadata(model, wireFeed, request);
+ buildFeedEntries(model, wireFeed, request, response);
+
+ response.setContentType(getContentType());
+ if (!StringUtils.hasText(wireFeed.getEncoding())) {
+ wireFeed.setEncoding("UTF-8");
+ }
+
+ WireFeedOutput feedOutput = new WireFeedOutput();
+ ServletOutputStream out = response.getOutputStream();
+ feedOutput.output(wireFeed, new OutputStreamWriter(out, wireFeed.getEncoding()));
+ out.flush();
+ }
+
+ /**
+ * Create a new feed to hold the entries.
+ *
+ * @return the newly created Feed instance
+ */
+ protected abstract T newFeed();
+
+ /**
+ * Populate the feed metadata (title, link, description, etc.).
+ *
+ * Default is an empty implementation. Subclasses can override this method to add meta fields such as title, link
+ * description, etc.
+ *
+ * @param model the model, in case meta information must be populated from it
+ * @param feed the feed being populated
+ * @param request in case we need locale etc. Shouldn't look at attributes.
+ */
+ protected void buildFeedMetadata(Map model, T feed, HttpServletRequest request) {
+ }
+
+ /**
+ * Subclasses must implement this method to build feed entries, given the model.
+ *
+ * Note that the passed-in HTTP response is just supposed to be used for setting cookies or other HTTP headers. The
+ * built feed itself will automatically get written to the response after this method returns.
+ *
+ * @param model the model Map
+ * @param feed the feed to add entries to
+ * @param request in case we need locale etc. Shouldn't look at attributes.
+ * @param response in case we need to set cookies. Shouldn't write to it.
+ * @throws Exception any exception that occured during building
+ */
+ protected abstract void buildFeedEntries(Map model,
+ T feed,
+ HttpServletRequest request,
+ HttpServletResponse response) throws Exception;
+
+
+}
diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/feed/AbstractRssFeedView.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/feed/AbstractRssFeedView.java
new file mode 100644
index 00000000000..192e93a34ff
--- /dev/null
+++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/feed/AbstractRssFeedView.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2008 the original author or authors.
+ *
+ * Licensed 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.springframework.web.servlet.view.feed;
+
+import java.util.List;
+import java.util.Map;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import com.sun.syndication.feed.WireFeed;
+import com.sun.syndication.feed.rss.Channel;
+import com.sun.syndication.feed.rss.Item;
+
+/**
+ * Abstract superclass for RSS Feed views, using java.net's ROME package.
+ * Application-specific view classes will extend this class. The view will be held in the subclass itself, not in a
+ * template.
+ *
+ * Main entry points are the {@link #buildFeedMetadata(Map, WireFeed , HttpServletRequest)} and {@link
+ * #buildFeedItems(Map, HttpServletRequest, HttpServletResponse)}.
+ *
+ * @author Jettro Coenradie
+ * @author Sergio Bossa
+ * @author Arjen Poutsma
+ * @see #buildFeedMetadata(Map, WireFeed , HttpServletRequest)
+ * @see #buildFeedItems(Map, HttpServletRequest, HttpServletResponse)
+ * @since 3.0
+ */
+public abstract class AbstractRssFeedView extends AbstractFeedView {
+
+ /** Sets the appropriate content type: "application/rss+xml". */
+ protected AbstractRssFeedView() {
+ setContentType("application/rss+xml");
+ }
+
+ /**
+ * Create a new channel to hold the entries.
+ *
+ * By default returns an RSS 2.0 channel, but the subclass can specify any channel.
+ *
+ * @return the newly created Feed instance
+ * @see Channel#Channel(String)
+ */
+ @Override
+ protected Channel newFeed() {
+ return new Channel("rss_2.0");
+ }
+
+ /** Invokes {@link #buildFeedItems(Map, HttpServletRequest, HttpServletResponse)} to get a list of feed items. */
+ @Override
+ protected final void buildFeedEntries(Map model,
+ Channel channel,
+ HttpServletRequest request,
+ HttpServletResponse response) throws Exception {
+ List items = buildFeedItems(model, request, response);
+ channel.setItems(items);
+ }
+
+ /**
+ * Subclasses must implement this method to build feed items, given the model.
+ *
+ * Note that the passed-in HTTP response is just supposed to be used for setting cookies or other HTTP headers. The
+ * built feed itself will automatically get written to the response after this method returns.
+ *
+ * @param model the model Map
+ * @param request in case we need locale etc. Shouldn't look at attributes.
+ * @param response in case we need to set cookies. Shouldn't write to it.
+ * @return the feed items to be added to the feed
+ * @throws Exception any exception that occured during document building
+ * @see Item
+ */
+ protected abstract List buildFeedItems(Map model, HttpServletRequest request, HttpServletResponse response)
+ throws Exception;
+}
diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/feed/package.html b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/feed/package.html
new file mode 100644
index 00000000000..cf59b64759e
--- /dev/null
+++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/feed/package.html
@@ -0,0 +1,5 @@
+
+
+Support classes for feed generation, providing View implementations for Atom and RSS.
+
+
diff --git a/org.springframework.web.servlet/template.mf b/org.springframework.web.servlet/template.mf
index 687dc23a9f8..07833afe2c8 100644
--- a/org.springframework.web.servlet/template.mf
+++ b/org.springframework.web.servlet/template.mf
@@ -7,6 +7,7 @@ Import-Package:
org.apache.tiles.definition;version="[2.0.5.osgi, 3.0.0)";resolution:=optional,
org.apache.tiles.jsp.context;version="[2.0.5, 3.0.0)";resolution:=optional
Import-Template:
+ com.sun.syndication.*;version="[0.9.0, 0.9.1)";resolution:=optional,
com.lowagie.text.*;version="[2.0.8, 3.0.0)";resolution:=optional,
freemarker.*;version="[2.3.12, 3.0.0)";resolution:=optional,
javax.servlet;version="[2.4.0, 3.0.0)",