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)",