Index: src/main/java/info/magnolia/module/rssaggregator/servlet/FeedSyndicationServlet.java =================================================================== --- src/main/java/info/magnolia/module/rssaggregator/servlet/FeedSyndicationServlet.java Sun Feb 22 21:11:08 CET 2009 +++ src/main/java/info/magnolia/module/rssaggregator/servlet/FeedSyndicationServlet.java Sun Feb 22 21:11:08 CET 2009 @@ -0,0 +1,163 @@ +/** + * This file Copyright (c) 2008-2009 Magnolia International + * Ltd. (http://www.magnolia-cms.com). All rights reserved. + * + * + * This file is dual-licensed under both the Magnolia + * Network Agreement and the GNU General Public License. + * You may elect to use one or the other of these licenses. + * + * This file is distributed in the hope that it will be + * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the + * implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT. + * Redistribution, except as permitted by whichever of the GPL + * or MNA you select, is prohibited. + * + * 1. For the GPL license (GPL), you can redistribute and/or + * modify this file under the terms of the GNU General + * Public License, Version 3, as published by the Free Software + * Foundation. You should have received a copy of the GNU + * General Public License, Version 3 along with this program; + * if not, write to the Free Software Foundation, Inc., 51 + * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * 2. For the Magnolia Network Agreement (MNA), this file + * and the accompanying materials are made available under the + * terms of the MNA which accompanies this distribution, and + * is available at http://www.magnolia-cms.com/mna.html + * + * Any modifications to this file must keep this entire header + * intact. + * + */ +package info.magnolia.module.rssaggregator.servlet; + +import info.magnolia.cms.core.Content; +import info.magnolia.cms.core.ItemType; +import info.magnolia.content2bean.Content2BeanException; +import info.magnolia.content2bean.Content2BeanUtil; +import info.magnolia.module.rssaggregator.generator.Feed; +import info.magnolia.module.rssaggregator.generator.FeedGenerator; +import info.magnolia.module.rssaggregator.generator.FeedGeneratorFactory; +import info.magnolia.module.rssaggregator.generator.FeedGeneratorResolver; +import info.magnolia.module.rssaggregator.util.Assert; +import info.magnolia.module.rssaggregator.util.MagnoliaQueryOperations; +import info.magnolia.module.rssaggregator.util.MagnoliaTemplate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; +import static java.lang.String.*; +import java.util.Map; + +/** + * Writes an XML feed to the response. Based on the given request parameters, a FeedGenerator will be resolved and is + * used for generating an XML feed. The content of the feed will be written to the response with the appropriate + * character encoding. + * + * @author Rob van der Linden Vooren + * @see FeedGeneratorResolver + * @see FeedGenerator + */ +public class FeedSyndicationServlet extends AbstractServlet { + + protected Logger logger = LoggerFactory.getLogger(getClass()); + + private FeedGeneratorResolver feedGeneratorResolver; + private MagnoliaQueryOperations magnoliaTemplate; + + /** Construct a new AggregateFeedSyndicationController initialized with a {@link FeedGeneratorResolver}. */ + public FeedSyndicationServlet() { + feedGeneratorResolver = new FeedGeneratorResolver(); + magnoliaTemplate = new MagnoliaTemplate(); + } + + @Override + public void init() throws ServletException { + try { + registerConfiguredFeedGeneratorFactories(); + } catch (Exception e) { + throw new ServletException("Failed to register feed generator factories", e); + } + } + + /** + * Retrieve and register {@link FeedGeneratorFactory}s that have been configured through the Magnolia Admin + * Interface under a generators node. Each content node under the "generators" node, represents a + * FeedGeneratorFactory to register. The name of such a node is its "generatorName". Each FeedGeneratorFactory has a + * "class" property as well, which holds the fully qualified name of the class of the FeedGeneratorFactory. A + * configured node should like this: + *
+     * /modules/rssaggregator/config/generators           <- generators
+     *                                         /rss       < FeedGeneratoryFactory named "rss"
+     *                                             /class < FeedGeneratoryFactory FQN class
+     *                                         /news      < FeedGeneratoryFactory named "news"
+     *                                             /class < FeedGeneratoryFactory FQN class
+     *                                         /...etc
+     * 
+ * Note that you can have an any numbers of FeedGeneratorFactories configured you want. + */ + protected void registerConfiguredFeedGeneratorFactories() { + Map feedGeneratorFactories = loadFeedGeneratorFactories(); + for (Map.Entry entry : feedGeneratorFactories.entrySet()) { + String name = entry.getKey(); + FeedGeneratorFactory factory = entry.getValue(); + feedGeneratorResolver.registerFeedGeneratorFactory(name, factory); + if (logger.isInfoEnabled()) { + logger.info(format("Registered FeedGeneratorFactory '%s' for name '%s'", + factory.getClass().getName(), name)); + } + } + } + + @SuppressWarnings("unchecked") + protected Map loadFeedGeneratorFactories() { + String query = "/jcr:root/modules/rssaggregator/config/generators"; + Content generatorsNode = magnoliaTemplate.xpathQueryForContent("config", query, ItemType.CONTENTNODE); + try { + return Content2BeanUtil.toMap(generatorsNode, true, Map.class); + } catch (Content2BeanException c2be) { + throw new RuntimeException(format("Failed to convert generators node '%s'", query), c2be); + } + } + + public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { + @SuppressWarnings("unchecked") + FeedGenerator generator = feedGeneratorResolver.resolve(request.getParameterMap()); + Feed feed = generator.generate(); + writeToResponse(feed.getXml(), feed.getCharacterEncoding(), response); + } + + /** + * Write the given content to the response. + * + * @param content the content to write + * @param characterEncoding the character encoding to use + * @param response the response to write to + * @throws IOException if an IOException occurs + */ + protected void writeToResponse(String content, String characterEncoding, HttpServletResponse response) throws IOException { + response.setCharacterEncoding(characterEncoding); + response.setContentType("text/xml"); + PrintWriter writer = response.getWriter(); + writer.write((content == null) ? "" : content); + writer.flush(); + writer.close(); + } + + /** + * Set the {@link FeedGeneratorResolver} used for resolving the FeedGenerators. + * + * @param feedGeneratorResolver the generator resolver to be used (must not be null) + * @throws IllegalArgumentException if the given {@code generatorResolver} is null + */ + public void setFeedGeneratorResolver(FeedGeneratorResolver feedGeneratorResolver) { + Assert.notNull(feedGeneratorResolver, "'feedGeneratorResolver' must not be null"); + this.feedGeneratorResolver = feedGeneratorResolver; + } +} Index: src/main/java/info/magnolia/module/rssaggregator/importhandler/AggregateFilter.java =================================================================== --- src/main/java/info/magnolia/module/rssaggregator/importhandler/AggregateFilter.java (revision 22801) +++ src/main/java/info/magnolia/module/rssaggregator/importhandler/AggregateFilter.java Sun Feb 22 19:43:18 CET 2009 @@ -53,7 +53,7 @@ */ public class AggregateFilter { - private Logger log = LoggerFactory.getLogger(AggregateFilter.class); + private Logger logger = LoggerFactory.getLogger(getClass()); private final Set filters; @@ -71,8 +71,8 @@ try { propertyValue = MethodUtils.invokeExactMethod(entry, getterName, null); } catch (Exception e) { - if (log.isWarnEnabled()) { - log.warn("Property {} can't be retrieved from the feed and will be treated as if it was empty during filtering", property); + if (logger.isWarnEnabled()) { + logger.warn("Property {} can't be retrieved from the feed and will be treated as if it was empty during filtering", property); } } String regularExpression = filter.getRegularExpression(); Index: src/main/java/info/magnolia/module/rssaggregator/importhandler/FilterPredicateContentMapper.java =================================================================== --- src/main/java/info/magnolia/module/rssaggregator/importhandler/FilterPredicateContentMapper.java (revision 22801) +++ src/main/java/info/magnolia/module/rssaggregator/importhandler/FilterPredicateContentMapper.java Sat Feb 21 20:58:09 CET 2009 @@ -34,10 +34,11 @@ package info.magnolia.module.rssaggregator.importhandler; import info.magnolia.cms.core.Content; -import static info.magnolia.module.rssaggregator.importhandler.FilterPredicate.*; +import static info.magnolia.module.rssaggregator.importhandler.FilterPredicate.Condition; +import info.magnolia.module.rssaggregator.util.ContentMapper; import org.apache.commons.lang.StringUtils; -import static java.util.Arrays.*; +import static java.util.Arrays.asList; /** * Maps a content node representing a {@link FilterPredicate} to a FilterPredicate. Index: src/main/resources/mgnl-bootstrap/rssaggregator/config.modules.data.config.importers.rssaggregator.xml =================================================================== --- src/main/resources/mgnl-bootstrap/rssaggregator/config.modules.data.config.importers.rssaggregator.xml (revision 22801) +++ src/main/resources/mgnl-bootstrap/rssaggregator/config.modules.data.config.importers.rssaggregator.xml Sat Feb 21 21:08:53 CET 2009 @@ -2,8 +2,8 @@ + xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:fn_old="http://www.w3.org/2004/10/xpath-functions" + xmlns:sv="http://www.jcp.org/jcr/sv/1.0" xmlns:jcrfn="http://www.jcp.org/jcr/xpath-functions/1.0"> mgnl:contentNode @@ -19,9 +19,6 @@ false - - info.magnolia.module.rssaggregator.importhandler.SimpleRSSFeedFetcher - data @@ -45,7 +42,7 @@ 2008-07-03T07:03:50.946+02:00 - 2009-02-02T22:21:41.334+01:00 + 2009-02-21T21:06:53.484+01:00 @@ -149,8 +146,39 @@ 2008-07-03T07:03:50.962+02:00 - 2009-02-02T22:21:35.576+01:00 + 2009-02-21T21:07:30.375+01:00 + + + mgnl:contentNode + + + mix:lockable + + + a30106a2-2000-485d-b03b-66faebf8b85e + + + info.magnolia.module.rssaggregator.importhandler.FastRSSFeedFetcher + + + + mgnl:metaData + + + superuser + + + 2009-02-15T13:44:19.375+01:00 + + + 2009-02-21T21:08:07.328+01:00 + + + + - + + + Index: src/main/java/info/magnolia/module/rssaggregator/generator/AbstractSyndFeedGenerator.java =================================================================== --- src/main/java/info/magnolia/module/rssaggregator/generator/AbstractSyndFeedGenerator.java Sun Feb 22 21:16:11 CET 2009 +++ src/main/java/info/magnolia/module/rssaggregator/generator/AbstractSyndFeedGenerator.java Sun Feb 22 21:16:11 CET 2009 @@ -0,0 +1,125 @@ +/** + * This file Copyright (c) 2008-2009 Magnolia International + * Ltd. (http://www.magnolia-cms.com). All rights reserved. + * + * + * This file is dual-licensed under both the Magnolia + * Network Agreement and the GNU General Public License. + * You may elect to use one or the other of these licenses. + * + * This file is distributed in the hope that it will be + * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the + * implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT. + * Redistribution, except as permitted by whichever of the GPL + * or MNA you select, is prohibited. + * + * 1. For the GPL license (GPL), you can redistribute and/or + * modify this file under the terms of the GNU General + * Public License, Version 3, as published by the Free Software + * Foundation. You should have received a copy of the GNU + * General Public License, Version 3 along with this program; + * if not, write to the Free Software Foundation, Inc., 51 + * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * 2. For the Magnolia Network Agreement (MNA), this file + * and the accompanying materials are made available under the + * terms of the MNA which accompanies this distribution, and + * is available at http://www.magnolia-cms.com/mna.html + * + * Any modifications to this file must keep this entire header + * intact. + * + */ +package info.magnolia.module.rssaggregator.generator; + +import com.sun.syndication.feed.synd.SyndEntry; +import com.sun.syndication.feed.synd.SyndFeed; +import com.sun.syndication.feed.synd.SyndFeedImpl; +import com.sun.syndication.io.FeedException; +import com.sun.syndication.io.SyndFeedOutput; +import info.magnolia.module.rssaggregator.util.Assert; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static java.lang.String.*; +import java.util.List; + +/** + * Convenience base class providing plumbing required for generating a Feed from a {@link SyndFeed}. The generated feed + * will by default be of type "{@value #DEFAULT_FEEDTYPE}". Subclasses need implement the template methods {@link + * #loadFeedEntries()} and {@link #setFeedInfo(SyndFeed)} to have a Feed generated. + * + * @author Rob van der Linden Vooren + * @see FeedGenerator + * @see SyndFeed + * @see Feed + */ +public abstract class AbstractSyndFeedGenerator implements FeedGenerator { + + protected static final String DEFAULT_FEEDTYPE = "rss_2.0"; + protected static final String DEFAULT_ENCODING = "UTF-8"; + + protected Logger logger = LoggerFactory.getLogger(getClass()); + + /** + * Generate a SyndFeed. + * + * @return the generated SyndFeed + * @throws FeedGenerationException when an exception occurs while generating the aggregate feed + */ + public Feed generate() throws FeedGenerationException { + try { + SyndFeed syndFeed = newSyndFeed(); + syndFeed.setFeedType(DEFAULT_FEEDTYPE); + syndFeed.setEntries(loadFeedEntries()); + setFeedInfo(syndFeed); + + String xml = syndFeedToXml(syndFeed); + return new Feed(xml, DEFAULT_ENCODING); + } catch (Exception e) { + String message = format("Failed to generate Feed using generator '%s'", getClass().getName()); + logger.error(message, e); + throw new FeedGenerationException(message, e); + } + } + + protected String syndFeedToXml(SyndFeed feed) throws FeedException { + SyndFeedOutput feedOutput = new SyndFeedOutput(); + return feedOutput.outputString(feed); + } + + /** + * Construct a new SyndFeed instance. + * + * @return the new SyndFeed instance + */ + protected SyndFeedImpl newSyndFeed() { + return new SyndFeedImpl(); + } + + /** + * Template method for subclasses to implement in order to provide the feed entries to include in the Feed to + * generate. + * + * @return the feed entries to include in the Feed to generate + */ + public abstract List loadFeedEntries(); + + /** + * Template method for subclasses are to override in order to set appropriate Feed meta data. Typical use cases + * would be to set the {@link SyndFeed#setTitle(String) title} , {@link SyndFeed#setLink(String) link} and {@link + * SyndFeed#setDescription(String) description}. The given {@code feed} will never be null. + * + * @param feed the syndication feed that is generated to set the feed meta data for + */ + public abstract void setFeedInfo(SyndFeed feed); + + // Getters & setters + + /** for testing */ + protected void setLogger(Logger logger) { + Assert.notNull(logger, "'logger' must not be null"); + this.logger = logger; + } +} Index: src/main/java/info/magnolia/module/rssaggregator/servlet/AbstractServlet.java =================================================================== --- src/main/java/info/magnolia/module/rssaggregator/servlet/AbstractServlet.java Sun Feb 22 11:29:41 CET 2009 +++ src/main/java/info/magnolia/module/rssaggregator/servlet/AbstractServlet.java Sun Feb 22 11:29:41 CET 2009 @@ -0,0 +1,101 @@ +/** + * This file Copyright (c) 2008-2009 Magnolia International + * Ltd. (http://www.magnolia-cms.com). All rights reserved. + * + * + * This file is dual-licensed under both the Magnolia + * Network Agreement and the GNU General Public License. + * You may elect to use one or the other of these licenses. + * + * This file is distributed in the hope that it will be + * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the + * implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT. + * Redistribution, except as permitted by whichever of the GPL + * or MNA you select, is prohibited. + * + * 1. For the GPL license (GPL), you can redistribute and/or + * modify this file under the terms of the GNU General + * Public License, Version 3, as published by the Free Software + * Foundation. You should have received a copy of the GNU + * General Public License, Version 3 along with this program; + * if not, write to the Free Software Foundation, Inc., 51 + * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * 2. For the Magnolia Network Agreement (MNA), this file + * and the accompanying materials are made available under the + * terms of the MNA which accompanies this distribution, and + * is available at http://www.magnolia-cms.com/mna.html + * + * Any modifications to this file must keep this entire header + * intact. + * + */ +package info.magnolia.module.rssaggregator.servlet; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Convenience base servlet for simple request handling. As such provides a {@link #handleRequest(HttpServletRequest, + * HttpServletResponse) main entry point} for handling POST and GET requests. + * + * @author Rob van der Linden Vooren + */ +public abstract class AbstractServlet extends HttpServlet { + + /** + * Delegate POST requests to {@link #processRequest}. + * + * @see #handleRequest + */ + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + processRequest(request, response); + } + + /** + * Delegate GET requests to {@link #processRequest}. + * + * @see #handleRequest + */ + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + processRequest(request, response); + } + + /** + * Delegates to template method {@link #handleRequest(HttpServletRequest, HttpServletResponse) handleRequest} which + * handles actual handling of the request. Additionaly wraps exceptions other than IOException and ServletException + * in a ServletException. + * + * @param request the request to handle + * @param response the associated response + * @throws IOException if an input or output error is detected when the servlet handles the request + * @throws ServletException if the request could not be handled + */ + protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + try { + handleRequest(request, response); + } catch (ServletException se) { + throw se; + } catch (IOException ioe) { + throw ioe; + } catch (Throwable t) { + throw new ServletException("Failed to process request", t); + } + } + + /** + * Subclass need implement this method in order to handle the request. + * + * @param request the request to handle + * @param response the associated response + * @throws Exception if an exception occurs during request handling + */ + public abstract void handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception; + +} Index: src/main/java/info/magnolia/module/rssaggregator/generator/FeedGenerationException.java =================================================================== --- src/main/java/info/magnolia/module/rssaggregator/generator/FeedGenerationException.java Sun Feb 22 21:14:53 CET 2009 +++ src/main/java/info/magnolia/module/rssaggregator/generator/FeedGenerationException.java Sun Feb 22 21:14:53 CET 2009 @@ -0,0 +1,46 @@ +/** + * This file Copyright (c) 2008-2009 Magnolia International + * Ltd. (http://www.magnolia-cms.com). All rights reserved. + * + * + * This file is dual-licensed under both the Magnolia + * Network Agreement and the GNU General Public License. + * You may elect to use one or the other of these licenses. + * + * This file is distributed in the hope that it will be + * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the + * implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT. + * Redistribution, except as permitted by whichever of the GPL + * or MNA you select, is prohibited. + * + * 1. For the GPL license (GPL), you can redistribute and/or + * modify this file under the terms of the GNU General + * Public License, Version 3, as published by the Free Software + * Foundation. You should have received a copy of the GNU + * General Public License, Version 3 along with this program; + * if not, write to the Free Software Foundation, Inc., 51 + * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * 2. For the Magnolia Network Agreement (MNA), this file + * and the accompanying materials are made available under the + * terms of the MNA which accompanies this distribution, and + * is available at http://www.magnolia-cms.com/mna.html + * + * Any modifications to this file must keep this entire header + * intact. + * + */ +package info.magnolia.module.rssaggregator.generator; + +/** + * Thrown when an exception occurs while generating a feed. + * + * @author Rob van der Linden Vooren + */ +public class FeedGenerationException extends RuntimeException { + + public FeedGenerationException(String message, Throwable cause) { + super(message, cause); + } +} Index: src/test/java/info/magnolia/module/rssaggregator/importhandler/SimpleRSSFeedFetcherTest.java =================================================================== --- src/test/java/info/magnolia/module/rssaggregator/importhandler/SimpleRSSFeedFetcherTest.java (revision 22801) +++ src/test/java/info/magnolia/module/rssaggregator/importhandler/SimpleRSSFeedFetcherTest.java Sun Feb 22 19:21:11 CET 2009 @@ -45,7 +45,6 @@ import java.util.Set; - /** * Tests {@link SimpleRSSFeedFetcher}. * @@ -54,7 +53,7 @@ public class SimpleRSSFeedFetcherTest { private SimpleRSSFeedFetcher feedFetcher; - private Logger logger = LoggerFactory.getLogger(SimpleRSSFeedFetcherTest.class); + private Logger logger = LoggerFactory.getLogger(getClass()); @Before public void before() { @@ -77,7 +76,7 @@ expect(feedFetcher.fetchFeedChannel(channel)).andReturn(expectedFetchResult); replay(feedFetcher); - Set fetchedAggregates = feedFetcher.fetchAggregate(aggregates); + Set fetchedAggregates = feedFetcher.fetchAggregateFeeds(aggregates); verify(feedFetcher); AggregateFeed fetchedAggregateFeed = fetchedAggregates.iterator().next(); Index: src/test/java/info/magnolia/module/rssaggregator/servlet/FeedSyndicationServletTest.java =================================================================== --- src/test/java/info/magnolia/module/rssaggregator/servlet/FeedSyndicationServletTest.java Sun Feb 22 18:51:26 CET 2009 +++ src/test/java/info/magnolia/module/rssaggregator/servlet/FeedSyndicationServletTest.java Sun Feb 22 18:51:26 CET 2009 @@ -0,0 +1,94 @@ +/** + * This file Copyright (c) 2008-2009 Magnolia International + * Ltd. (http://www.magnolia-cms.com). All rights reserved. + * + * + * This file is dual-licensed under both the Magnolia + * Network Agreement and the GNU General Public License. + * You may elect to use one or the other of these licenses. + * + * This file is distributed in the hope that it will be + * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the + * implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT. + * Redistribution, except as permitted by whichever of the GPL + * or MNA you select, is prohibited. + * + * 1. For the GPL license (GPL), you can redistribute and/or + * modify this file under the terms of the GNU General + * Public License, Version 3, as published by the Free Software + * Foundation. You should have received a copy of the GNU + * General Public License, Version 3 along with this program; + * if not, write to the Free Software Foundation, Inc., 51 + * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * 2. For the Magnolia Network Agreement (MNA), this file + * and the accompanying materials are made available under the + * terms of the MNA which accompanies this distribution, and + * is available at http://www.magnolia-cms.com/mna.html + * + * Any modifications to this file must keep this entire header + * intact. + * + */ +package info.magnolia.module.rssaggregator.servlet; + +import info.magnolia.module.rssaggregator.generator.Feed; +import info.magnolia.module.rssaggregator.generator.FeedGenerator; +import info.magnolia.module.rssaggregator.generator.FeedGeneratorResolver; +import static org.easymock.classextension.EasyMock.*; +import org.junit.Before; +import org.junit.Test; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import static java.util.Collections.*; +import java.util.Map; + +/** + * Tests {@link FeedSyndicationServlet}. + * + * @author Rob van der Linden Vooren + */ +public class FeedSyndicationServletTest { + + private FeedSyndicationServlet servlet; + private HttpServletRequest mockRequest; + private HttpServletResponse mockResponse; + private FeedGeneratorResolver mockFeedGeneratorResolver; + private FeedGenerator mockFeedGenerator; + + @Before + public void before() { + servlet = new FeedSyndicationServlet(); + mockRequest = createMock("mockHttpServletRequest", HttpServletRequest.class); + mockResponse = createMock("mockHttpServletResponse", HttpServletResponse.class); + mockFeedGeneratorResolver = createMock("mockFeedGeneratorResolver", FeedGeneratorResolver.class); + mockFeedGenerator = createMock("mockFeedGenerator", FeedGenerator.class); + } + + @Test + @SuppressWarnings("unchecked") + public void testHandleRequest() throws Exception { + servlet = createMock("partialMockFeedSyndicationServlet", FeedSyndicationServlet.class, + FeedSyndicationServlet.class.getDeclaredMethod("writeToResponse", String.class, String.class, + HttpServletResponse.class) + ); + servlet.setFeedGeneratorResolver(mockFeedGeneratorResolver); + + final String content = ""; + final String encoding = "UTF-8"; + final Feed expectedFeed = new Feed(content, encoding); + final Map expectedParameters = emptyMap(); + + expect(mockRequest.getParameterMap()).andReturn(expectedParameters); + expect(mockFeedGeneratorResolver.resolve(expectedParameters)).andReturn(mockFeedGenerator); + expect(mockFeedGenerator.generate()).andReturn(expectedFeed); + servlet.writeToResponse(content, encoding, mockResponse); + replay(mockRequest, mockFeedGeneratorResolver, mockFeedGenerator, servlet); + + servlet.handleRequest(mockRequest, mockResponse); + + verify(mockRequest, mockFeedGeneratorResolver, mockFeedGenerator, servlet); + } +} Index: src/main/resources/META-INF/magnolia/rssaggregator.xml =================================================================== --- src/main/resources/META-INF/magnolia/rssaggregator.xml (revision 22801) +++ src/main/resources/META-INF/magnolia/rssaggregator.xml Sun Feb 22 19:52:12 CET 2009 @@ -1,31 +1,44 @@ - rssaggregator - RSS Aggregator - info.magnolia.module.rssaggregator.RSSAggregator - info.magnolia.module.rssaggregator.RSSAggregatorVersionHandler - ${project.version} + rssaggregator + RSS Aggregator + info.magnolia.module.rssaggregator.RSSAggregator + info.magnolia.module.rssaggregator.RSSAggregatorVersionHandler + ${project.version} - - - info.magnolia.module.rssaggregator.importhandler.RSSFeedFetcher - info.magnolia.module.rssaggregator.importhandler.SimpleRSSFeedFetcher - - + + + info.magnolia.module.rssaggregator.importhandler.RSSFeedFetcher + info.magnolia.module.rssaggregator.importhandler.SimpleRSSFeedFetcher + + + - - - core - 4.0/* - - - observation - 1.1/* - true - - - data - 1.3/* - - + + + core + 4.0/* + + + observation + 1.1/* + true + + + data + 1.3/* + + + + + + FeedSyndicationServlet + info.magnolia.module.rssaggregator.servlet.FeedSyndicationServlet + Responsible for RSS Feed syndication + + /rss + + + + Index: src/main/java/info/magnolia/module/rssaggregator/generator/FeedGeneratorConstructionException.java =================================================================== --- src/main/java/info/magnolia/module/rssaggregator/generator/FeedGeneratorConstructionException.java Sun Feb 22 19:49:06 CET 2009 +++ src/main/java/info/magnolia/module/rssaggregator/generator/FeedGeneratorConstructionException.java Sun Feb 22 19:49:06 CET 2009 @@ -0,0 +1,47 @@ +/** + * This file Copyright (c) 2008-2009 Magnolia International + * Ltd. (http://www.magnolia-cms.com). All rights reserved. + * + * + * This file is dual-licensed under both the Magnolia + * Network Agreement and the GNU General Public License. + * You may elect to use one or the other of these licenses. + * + * This file is distributed in the hope that it will be + * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the + * implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT. + * Redistribution, except as permitted by whichever of the GPL + * or MNA you select, is prohibited. + * + * 1. For the GPL license (GPL), you can redistribute and/or + * modify this file under the terms of the GNU General + * Public License, Version 3, as published by the Free Software + * Foundation. You should have received a copy of the GNU + * General Public License, Version 3 along with this program; + * if not, write to the Free Software Foundation, Inc., 51 + * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * 2. For the Magnolia Network Agreement (MNA), this file + * and the accompanying materials are made available under the + * terms of the MNA which accompanies this distribution, and + * is available at http://www.magnolia-cms.com/mna.html + * + * Any modifications to this file must keep this entire header + * intact. + * + */ +package info.magnolia.module.rssaggregator.generator; + +/** + * Thrown when a FeedGenerator cannot be constructed. + * + * @author Rob van der Linden Vooren + */ +public class FeedGeneratorConstructionException extends RuntimeException { + + /** {@inheritDoc} */ + public FeedGeneratorConstructionException(String message, Throwable cause) { + super(message, cause); + } +} Index: src/test/java/info/magnolia/module/rssaggregator/util/MagnoliaTemplateTest.java =================================================================== --- src/test/java/info/magnolia/module/rssaggregator/util/MagnoliaTemplateTest.java Sun Feb 22 19:02:34 CET 2009 +++ src/test/java/info/magnolia/module/rssaggregator/util/MagnoliaTemplateTest.java Sun Feb 22 19:02:34 CET 2009 @@ -0,0 +1,215 @@ +/** + * This file Copyright (c) 2008-2009 Magnolia International + * Ltd. (http://www.magnolia-cms.com). All rights reserved. + * + * + * This file is dual-licensed under both the Magnolia + * Network Agreement and the GNU General Public License. + * You may elect to use one or the other of these licenses. + * + * This file is distributed in the hope that it will be + * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the + * implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT. + * Redistribution, except as permitted by whichever of the GPL + * or MNA you select, is prohibited. + * + * 1. For the GPL license (GPL), you can redistribute and/or + * modify this file under the terms of the GNU General + * Public License, Version 3, as published by the Free Software + * Foundation. You should have received a copy of the GNU + * General Public License, Version 3 along with this program; + * if not, write to the Free Software Foundation, Inc., 51 + * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * 2. For the Magnolia Network Agreement (MNA), this file + * and the accompanying materials are made available under the + * terms of the MNA which accompanies this distribution, and + * is available at http://www.magnolia-cms.com/mna.html + * + * Any modifications to this file must keep this entire header + * intact. + * + */ +package info.magnolia.module.rssaggregator.util; + +import info.magnolia.cms.core.Content; +import info.magnolia.cms.core.ItemType; +import static info.magnolia.cms.core.ItemType.*; +import info.magnolia.cms.core.search.Query; +import static org.easymock.classextension.EasyMock.*; +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Test; + +import javax.jcr.RepositoryException; +import static java.util.Arrays.*; +import java.util.Collection; +import static java.util.Collections.*; +import java.util.List; + +/** + * Tests {@link MagnoliaTemplate}. + * + * @author Rob van der Linden Vooren + */ +public class MagnoliaTemplateTest { + + private MagnoliaTemplate template; + + @Before + public void before() { + template = new MagnoliaTemplate(); + } + + @Test + @SuppressWarnings("unchecked") + public void testQueryForList() throws Exception { + template = createMock(MagnoliaTemplate.class, + MagnoliaTemplate.class.getDeclaredMethod("queryInternal", String.class, String.class, String.class, ItemType.class) + ); + + Content dummyContent = createMock(Content.class); + ContentMapper mockMapper = createMock(ContentMapper.class); + Collection contents = asList(dummyContent); + + expect(template.queryInternal("repo", "query", "xpath", CONTENTNODE)).andReturn(contents); + expect(mockMapper.map(dummyContent)).andReturn("mapping result #1"); + replay(template, mockMapper); + + List results = template.queryForList("repo", "query", "xpath", CONTENTNODE, mockMapper); + assertEquals("mapping result #1", results.get(0)); + verify(template, mockMapper); + } + + @Test + @SuppressWarnings("unchecked") + public void testQueryForObject() throws Exception { + template = createMock(MagnoliaTemplate.class, + MagnoliaTemplate.class.getDeclaredMethod("queryInternal", String.class, String.class, String.class, ItemType.class) + ); + + Content dummyContent = createMock(Content.class); + ContentMapper mockMapper = createMock(ContentMapper.class); + Collection contents = asList(dummyContent); + + expect(template.queryInternal("repo", "query", "xpath", CONTENTNODE)).andReturn(contents); + expect(mockMapper.map(dummyContent)).andReturn("mapping result #1"); + replay(template, mockMapper); + + String result = (String) template.queryForObject("repo", "query", "xpath", CONTENTNODE, mockMapper); + assertEquals("mapping result #1", result); + verify(template, mockMapper); + } + + @Test + @SuppressWarnings("unchecked") + public void testQueryForObject_returnsNullOnZeroResults() throws Exception { + template = createMock(MagnoliaTemplate.class, + MagnoliaTemplate.class.getDeclaredMethod("queryInternal", String.class, String.class, String.class, ItemType.class) + ); + + ContentMapper mockMapper = createMock(ContentMapper.class); + Collection contents = emptyList(); + + expect(template.queryInternal("repo", "query", "xpath", CONTENTNODE)).andReturn(contents); + replay(template, mockMapper); + + assertNull(template.queryForObject("repo", "query", "xpath", CONTENTNODE, mockMapper)); + verify(template, mockMapper); + } + + @Test + @SuppressWarnings("unchecked") + public void testQueryForObject_throwsIncorrectResultSizeDataAccessExceptionOnMultipleResults() throws Exception { + template = createMock(MagnoliaTemplate.class, + MagnoliaTemplate.class.getDeclaredMethod("queryInternal", String.class, String.class, String.class, ItemType.class) + ); + + Content dummyContent = createMock(Content.class); + ContentMapper mockMapper = createMock(ContentMapper.class); + Collection contents = asList(dummyContent, dummyContent); + + expect(template.queryInternal("repo", "query", "xpath", CONTENTNODE)).andReturn(contents); + expect(mockMapper.map(dummyContent)).andReturn("mapping result #1"); + expect(mockMapper.map(dummyContent)).andReturn("mapping result #2"); + replay(template, mockMapper); + + try { + template.queryForObject("repo", "query", "xpath", CONTENTNODE, mockMapper); + fail("IncorrectResultSizeDataAccessException expected"); + } catch (IncorrectResultSizeDataAccessException exception) { + assertEquals(1, exception.getExpectedSize()); + assertEquals(2, exception.getActualSize()); + verify(template, mockMapper); + } + } + + @Test + @SuppressWarnings("unchecked") + public void testXpathQueryForList() throws Exception { + template = createMock(MagnoliaTemplate.class, + MagnoliaTemplate.class.getDeclaredMethod("queryForList", String.class, String.class, String.class, ItemType.class, ContentMapper.class) + ); + + List expectedResults = emptyList(); + ContentMapper dummyMapper = createMock(ContentMapper.class); + + expect(template.queryForList("repository", "query", Query.XPATH, ItemType.CONTENT, dummyMapper)).andReturn(expectedResults); + replay(template); + + assertSame(expectedResults, template.xpathQueryForList("repository", "query", ItemType.CONTENT, dummyMapper)); + verify(template); + } + + @Test + @SuppressWarnings("unchecked") + public void testXpathQueryForObject() throws Exception { + template = createMock(MagnoliaTemplate.class, + MagnoliaTemplate.class.getDeclaredMethod("queryForObject", String.class, String.class, String.class, ItemType.class, ContentMapper.class) + ); + + ContentMapper dummyMapper = createMock(ContentMapper.class); + String expectedResult = "result"; + + expect(template.queryForObject("repository", "query", Query.XPATH, ItemType.CONTENT, dummyMapper)).andReturn(expectedResult); + replay(template); + + assertSame(expectedResult, template.xpathQueryForObject("repository", "query", ItemType.CONTENT, dummyMapper)); + verify(template); + } + + @Test + public void testQueryInternal() throws Exception { + template = createMock(MagnoliaTemplate.class, + MagnoliaTemplate.class.getDeclaredMethod("doExceptionThrowingQuery", String.class, String.class, String.class, ItemType.class) + ); + + Collection expectedResults = emptyList(); + expect(template.doExceptionThrowingQuery("repository", "query", Query.XPATH, ItemType.CONTENT)).andReturn(expectedResults); + replay(template); + + assertSame(expectedResults, template.queryInternal("repository", "query", Query.XPATH, ItemType.CONTENT)); + verify(template); + } + + @Test + @SuppressWarnings({"ThrowableInstanceNeverThrown"}) + public void testQueryInternal_translatesRepositoryExceptionToDataAccessException() throws Exception { + template = createMock(MagnoliaTemplate.class, + MagnoliaTemplate.class.getDeclaredMethod("doExceptionThrowingQuery", String.class, String.class, String.class, ItemType.class) + ); + + RepositoryException expectedException = new RepositoryException("supposedly thrown"); + expect(template.doExceptionThrowingQuery("repository", "query", Query.XPATH, ItemType.CONTENT)).andThrow(expectedException); + replay(template); + + try { + template.queryInternal("repository", "query", Query.XPATH, ItemType.CONTENT); + fail("DataAccessException expected"); + } catch (DataAccessException actual) { + assertSame(expectedException, actual.getCause()); + verify(template); + } + } +} Index: src/main/java/info/magnolia/module/rssaggregator/util/MagnoliaTemplate.java =================================================================== --- src/main/java/info/magnolia/module/rssaggregator/util/MagnoliaTemplate.java Sun Feb 22 19:43:05 CET 2009 +++ src/main/java/info/magnolia/module/rssaggregator/util/MagnoliaTemplate.java Sun Feb 22 19:43:05 CET 2009 @@ -0,0 +1,167 @@ +/** + * This file Copyright (c) 2008-2009 Magnolia International + * Ltd. (http://www.magnolia-cms.com). All rights reserved. + * + * + * This file is dual-licensed under both the Magnolia + * Network Agreement and the GNU General Public License. + * You may elect to use one or the other of these licenses. + * + * This file is distributed in the hope that it will be + * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the + * implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT. + * Redistribution, except as permitted by whichever of the GPL + * or MNA you select, is prohibited. + * + * 1. For the GPL license (GPL), you can redistribute and/or + * modify this file under the terms of the GNU General + * Public License, Version 3, as published by the Free Software + * Foundation. You should have received a copy of the GNU + * General Public License, Version 3 along with this program; + * if not, write to the Free Software Foundation, Inc., 51 + * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * 2. For the Magnolia Network Agreement (MNA), this file + * and the accompanying materials are made available under the + * terms of the MNA which accompanies this distribution, and + * is available at http://www.magnolia-cms.com/mna.html + * + * Any modifications to this file must keep this entire header + * intact. + * + */ +package info.magnolia.module.rssaggregator.util; + +import info.magnolia.cms.core.Content; +import info.magnolia.cms.core.ItemType; +import info.magnolia.cms.core.search.Query; +import info.magnolia.cms.util.QueryUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.RepositoryException; +import static java.lang.String.*; +import java.util.ArrayList; +import java.util.Collection; +import static java.util.Collections.*; +import java.util.List; + +/** + * Convenience class to simplify use of Magnolia queries and the mapping of their results to types.

Note that all + * queries performed by this class are logged at DEBUG level for convenient debugging when necessary. + * + * @author Rob van der Linden Vooren + * @see ContentMapper + */ +public class MagnoliaTemplate implements MagnoliaQueryOperations { + + /** Logger for this instance. */ + protected Logger logger = LoggerFactory.getLogger(getClass()); + + /** {@inheritDoc} */ + public List queryForList(String repository, String query, String language, ItemType type, ContentMapper mapper) { + return query(repository, query, language, type, mapper); + } + + /** {@inheritDoc} */ + public T queryForObject(String repository, String query, String language, ItemType type, ContentMapper mapper) { + List results = query(repository, query, language, type, mapper); + return singleResult(results); + } + + /** {@inheritDoc} */ + public List xpathQueryForList(String repository, String query, ItemType type, ContentMapper mapper) throws DataAccessException { + return queryForList(repository, query, Query.XPATH, type, mapper); + } + + /** {@inheritDoc} */ + public T xpathQueryForObject(String repository, String query, ItemType type, ContentMapper mapper) throws DataAccessException { + return queryForObject(repository, query, Query.XPATH, type, mapper); + } + + /** {@inheritDoc} */ + public Content xpathQueryForContent(String repository, String query, ItemType type) throws DataAccessException { + return queryForObject(repository, query, Query.XPATH, type, new ContentMapper() { + public Content map(Content content) throws RepositoryException { + return content; + } + }); + } + + // Helper methods + + protected List query(String repository, String query, String language, ItemType type, ContentMapper mapper) throws DataAccessException { + Collection contents = queryInternal(repository, query, language, type); + if (contents.isEmpty()) { + return emptyList(); + } + List result = new ArrayList(contents.size()); + for (Content content : contents) { + try { + result.add(mapper.map(content)); + } catch (RepositoryException re) { + throw new DataAccessException(String.format("Failed to map content '%s' using mapper '%s'", content, mapper.getClass())); + } + } + return result; + } + + /** + * Performs the actual query execution. Purposely delegates to {@link QueryUtil#exceptionThrowingQuery(String, + * String, String, String) QueryUtil.exceptionThrowingQuery(..)} in order to provide flexibility with regards to + * error handling, by translating any {@link RepositoryException} into an unchecked {@link DataAccessException}. + * This allows for optional exception handling and logging, making it a better alternative then using {@link + * QueryUtil#query(String, String) QueryUtil.query(..)} direcltly, which logs the error and then directly swallows + * the {@link RepositoryException exception}. Execute a query for a result list of type T, for the given query. + * + * @param repository the repository to execute query against + * @param query query to execute + * @param language query language (either {@value Query#XPATH} or {@value Query#SQL}). + * @param type the type of the item to query for + * @return the result object of the required type, or {@code null} in case of a {@code null} query + * @throws DataAccessException if there is any problem executing the query + */ + protected Collection queryInternal(String repository, String query, String language, ItemType type) { + try { + return doExceptionThrowingQuery(repository, query, language, type); + } catch (RepositoryException re) { + throw new DataAccessException("A repository exception occurred", re); + } + } + + @SuppressWarnings("unchecked") + protected Collection doExceptionThrowingQuery(String repository, String query, String language, ItemType type) throws RepositoryException { + if (logger.isDebugEnabled()) { + logger.debug(format("Executing %s query for '%s' in repository '%s': \"%s\"", language, + type.toString(), repository, query) + ); + } + Collection results = (Collection) QueryUtil.exceptionThrowingQuery(repository, query, language, type.toString()); + if (logger.isDebugEnabled()) { + logger.debug(format("Query returned %s result(s)", (results == null) ? null : results.size())); + } + return results; + } + + /** + * Return a single result object from the given collection or {@code null} if the collection is empty or null. + * + * @param results the result collection (may be {@code null}) + * @return the single result object, or {@code null} when no result + * @throws IncorrectResultSizeDataAccessException + * if collection contains more than one element + */ + @SuppressWarnings("unchecked") + protected static T singleResult(Collection results) throws IncorrectResultSizeDataAccessException { + int size = (results == null ? 0 : results.size()); + if (size == 0) { + return null; + } + if (results.size() > 1) { + throw new IncorrectResultSizeDataAccessException(1, size); + } + return results.iterator().next(); + } + +} Index: src/test/java/info/magnolia/module/rssaggregator/generator/SyndFeedGeneratorTest.java =================================================================== --- src/test/java/info/magnolia/module/rssaggregator/generator/SyndFeedGeneratorTest.java Sun Feb 22 19:48:13 CET 2009 +++ src/test/java/info/magnolia/module/rssaggregator/generator/SyndFeedGeneratorTest.java Sun Feb 22 19:48:13 CET 2009 @@ -0,0 +1,89 @@ +/** + * This file Copyright (c) 2008-2009 Magnolia International + * Ltd. (http://www.magnolia-cms.com). All rights reserved. + * + * + * This file is dual-licensed under both the Magnolia + * Network Agreement and the GNU General Public License. + * You may elect to use one or the other of these licenses. + * + * This file is distributed in the hope that it will be + * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the + * implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT. + * Redistribution, except as permitted by whichever of the GPL + * or MNA you select, is prohibited. + * + * 1. For the GPL license (GPL), you can redistribute and/or + * modify this file under the terms of the GNU General + * Public License, Version 3, as published by the Free Software + * Foundation. You should have received a copy of the GNU + * General Public License, Version 3 along with this program; + * if not, write to the Free Software Foundation, Inc., 51 + * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * 2. For the Magnolia Network Agreement (MNA), this file + * and the accompanying materials are made available under the + * terms of the MNA which accompanies this distribution, and + * is available at http://www.magnolia-cms.com/mna.html + * + * Any modifications to this file must keep this entire header + * intact. + * + */ +package info.magnolia.module.rssaggregator.generator; + +import com.sun.syndication.feed.synd.SyndEntry; +import com.sun.syndication.feed.synd.SyndFeed; +import com.sun.syndication.feed.synd.SyndFeedImpl; +import static org.easymock.classextension.EasyMock.*; +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collections; +import java.util.List; + +/** + * Tests {@link AbstractSyndFeedGenerator}. + * + * @author Rob van der Linden Vooren + */ +public class SyndFeedGeneratorTest { + + private Logger logger = LoggerFactory.getLogger(getClass()); + + private AbstractSyndFeedGenerator generator; + + @Before + public void before() throws Exception { + generator = createStrictMock("partialMockGenerator", AbstractSyndFeedGenerator.class, + AbstractSyndFeedGenerator.class.getDeclaredMethod("newSyndFeed"), + AbstractSyndFeedGenerator.class.getDeclaredMethod("loadFeedEntries"), + AbstractSyndFeedGenerator.class.getDeclaredMethod("setFeedInfo", SyndFeed.class), + AbstractSyndFeedGenerator.class.getDeclaredMethod("syndFeedToXml", SyndFeed.class) + ); + } + + @Test + public void testGenerate() throws Exception { + generator.setLogger(logger); + + List entries = Collections.emptyList(); + + SyndFeedImpl expectedSyndFeed = new SyndFeedImpl(); + expect(generator.newSyndFeed()).andReturn(expectedSyndFeed); + expect(generator.loadFeedEntries()).andReturn(entries); + generator.setFeedInfo(expectedSyndFeed); + expect(generator.syndFeedToXml(expectedSyndFeed)).andReturn(""); + replay(generator); + + Feed feed = generator.generate(); + + verify(generator); + assertEquals("", feed.getXml()); + assertEquals("UTF-8", feed.getCharacterEncoding()); + } +} Index: src/main/resources/mgnl-files/docroot/rssaggregator/ProduceFeed.jsp =================================================================== --- src/main/resources/mgnl-files/docroot/rssaggregator/ProduceFeed.jsp (revision 22801) +++ src/main/resources/mgnl-files/docroot/rssaggregator/ProduceFeed.jsp (revision 22801) @@ -1,8 +0,0 @@ - - - - -new FeedGenerator().writeBlogRSS("rss_2.0", pageContext.getOut(), request.getParameter("feedPath")); - - \ No newline at end of file Index: src/main/java/info/magnolia/module/rssaggregator/importhandler/FastRSSFeedFetcher.java =================================================================== --- src/main/java/info/magnolia/module/rssaggregator/importhandler/FastRSSFeedFetcher.java Sun Feb 22 20:41:10 CET 2009 +++ src/main/java/info/magnolia/module/rssaggregator/importhandler/FastRSSFeedFetcher.java Sun Feb 22 20:41:10 CET 2009 @@ -0,0 +1,139 @@ +/** + * This file Copyright (c) 2008-2009 Magnolia International + * Ltd. (http://www.magnolia-cms.com). All rights reserved. + * + * + * This file is dual-licensed under both the Magnolia + * Network Agreement and the GNU General Public License. + * You may elect to use one or the other of these licenses. + * + * This file is distributed in the hope that it will be + * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the + * implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT. + * Redistribution, except as permitted by whichever of the GPL + * or MNA you select, is prohibited. + * + * 1. For the GPL license (GPL), you can redistribute and/or + * modify this file under the terms of the GNU General + * Public License, Version 3, as published by the Free Software + * Foundation. You should have received a copy of the GNU + * General Public License, Version 3 along with this program; + * if not, write to the Free Software Foundation, Inc., 51 + * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * 2. For the Magnolia Network Agreement (MNA), this file + * and the accompanying materials are made available under the + * terms of the MNA which accompanies this distribution, and + * is available at http://www.magnolia-cms.com/mna.html + * + * Any modifications to this file must keep this entire header + * intact. + * + */ +package info.magnolia.module.rssaggregator.importhandler; + +import com.sun.syndication.feed.synd.SyndFeed; +import com.sun.syndication.fetcher.impl.HashMapFeedInfoCache; +import com.sun.syndication.fetcher.impl.HttpURLFeedFetcher; +import info.magnolia.module.rssaggregator.util.Assert; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static java.lang.String.*; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +/** + * A multithreaded {@link RSSFeedFetcher} for feed retrieval over http that follows redirects and supports + * conditional gets.

Will attempt to fetch all {@link FeedChannel feed channels} defined in the provided {@link + * AggregateFeed aggregate feeds}, processing {@value #NUMBER_OF_THREADS} feeds simultaneously. Failures to fetch a feed + * result for a given feed channel are logged at ERROR level. + * + * @author Rob van der Linden Vooren + * @see RSSFeedFetcher + */ +public class FastRSSFeedFetcher implements RSSFeedFetcher { + + private static final int NUMBER_OF_THREADS = 3; + + private final ExecutorService executorService; + + private final com.sun.syndication.fetcher.FeedFetcher feedFetcher; + + private Logger logger = LoggerFactory.getLogger(getClass()); + + public FastRSSFeedFetcher() { + executorService = Executors.newFixedThreadPool(NUMBER_OF_THREADS); + feedFetcher = new HttpURLFeedFetcher(HashMapFeedInfoCache.getInstance()); + } + + public Set fetchAggregateFeeds(Set aggregateFeeds) { + List futureList = new ArrayList(); + for (AggregateFeed aggregate : aggregateFeeds) { + for (FeedChannel channel : aggregate.getChannels()) { + FeedChannelFetchTask task = new FeedChannelFetchTask(channel, aggregate.getName()); + Future future = executorService.submit(task); + futureList.add(future); + } + } + // Each Future will block on get() when not done, thus ensuring this method will return only when all + // attempts to fetch feed aggregates have completed. + for (Future future : futureList) { + try { + future.get(); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } catch (ExecutionException ee) { + ee.printStackTrace(); + } + } + return aggregateFeeds; + } + + // Inner classes + + /** + * Task that fetches the given feed for the given {@code channel}. When the feed is fetched properly, the {@link + * FeedChannel#feed feed} property will be set with the result, otherwise it will be {@code null}. + */ + private class FeedChannelFetchTask implements Runnable { + + private final FeedChannel channel; + private final String aggregateName; + + public FeedChannelFetchTask(FeedChannel feedChannel, String aggregateName) { + Assert.notNull(feedChannel, "'feedChannel' must not be null"); + Assert.notNull(aggregateName, "'aggregateName' must not be null"); + this.channel = feedChannel; + this.aggregateName = aggregateName; + } + + public void run() { + final long threadId = Thread.currentThread().getId(); + try { + if (logger.isDebugEnabled()) { + logger.debug(format("Fetching feed channel '%s' for aggregate '%s' (Thread %s)", channel.getUrl(), + aggregateName, threadId)); + } + SyndFeed feed = feedFetcher.retrieveFeed(new URL(channel.getUrl())); + channel.setFeed(feed); + if (logger.isDebugEnabled()) { + logger.debug(format("Fetched feed channel '%s' for aggregate '%s' (Thread %s)", channel.getUrl(), + aggregateName, threadId)); + } + } catch (Exception e) { + if (logger.isErrorEnabled()) { + logger.error(format("Failed to fetch result for channel '%s' for aggregate '%s' (Thread %s)", + channel.getUrl(), aggregateName, threadId), e); + } + } + } + } +} Index: src/main/java/info/magnolia/module/rssaggregator/importhandler/RSSFeedFetcher.java =================================================================== --- src/main/java/info/magnolia/module/rssaggregator/importhandler/RSSFeedFetcher.java (revision 22801) +++ src/main/java/info/magnolia/module/rssaggregator/importhandler/RSSFeedFetcher.java Sat Feb 21 21:12:44 CET 2009 @@ -54,6 +54,6 @@ * @return the aggregates holding resulting feed content from the fetch * @throws IllegalArgumentException if aggregates are null */ - Set fetchAggregate(Set aggregateFeeds); + Set fetchAggregateFeeds(Set aggregateFeeds); } Index: src/main/java/info/magnolia/module/rssaggregator/generator/FeedGeneratorResolver.java =================================================================== --- src/main/java/info/magnolia/module/rssaggregator/generator/FeedGeneratorResolver.java Sun Feb 22 21:13:48 CET 2009 +++ src/main/java/info/magnolia/module/rssaggregator/generator/FeedGeneratorResolver.java Sun Feb 22 21:13:48 CET 2009 @@ -0,0 +1,100 @@ +/** + * This file Copyright (c) 2008-2009 Magnolia International + * Ltd. (http://www.magnolia-cms.com). All rights reserved. + * + * + * This file is dual-licensed under both the Magnolia + * Network Agreement and the GNU General Public License. + * You may elect to use one or the other of these licenses. + * + * This file is distributed in the hope that it will be + * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the + * implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT. + * Redistribution, except as permitted by whichever of the GPL + * or MNA you select, is prohibited. + * + * 1. For the GPL license (GPL), you can redistribute and/or + * modify this file under the terms of the GNU General + * Public License, Version 3, as published by the Free Software + * Foundation. You should have received a copy of the GNU + * General Public License, Version 3 along with this program; + * if not, write to the Free Software Foundation, Inc., 51 + * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * 2. For the Magnolia Network Agreement (MNA), this file + * and the accompanying materials are made available under the + * terms of the MNA which accompanies this distribution, and + * is available at http://www.magnolia-cms.com/mna.html + * + * Any modifications to this file must keep this entire header + * intact. + * + */ +package info.magnolia.module.rssaggregator.generator; + +import info.magnolia.module.rssaggregator.util.Assert; + +import static java.lang.String.*; +import java.util.Hashtable; +import java.util.Map; + +/** + * Responsible for resolving {@link FeedGenerator}s based on given parameters. In order to resolve a {@link + * FeedGenerator}, a {@link FeedGeneratorFactory} capable of building it must first be {@link + * #registerFeedGeneratorFactory(String, FeedGeneratorFactory) registered} with a given name. + * + * @author Rob van der Linden Vooren + * @see FeedGeneratorFactory + * @see FeedGenerator + */ +public class FeedGeneratorResolver { + + public static final String REQUIRED_PARAMETER = "generatorName"; + + private final Map feedGeneratorFactories; + + /** Construct new SyndFeedGenerator with an empty generator factory mapping. */ + public FeedGeneratorResolver() { + feedGeneratorFactories = new Hashtable(); + } + + /** + * Return a FeedGenerator instance based on the given {@code parameters}. As a minimal requirement, the incoming + * parameter map is expected to hold a parameter named "{@value #REQUIRED_PARAMETER}". Based on the value of + * this parameter the associated {@link FeedGenerator} is constructed and returned. + * + * @param parameterMap parameter map (must not be null) (key = name / values = 0..N values for parameter name) + * @return the SyndFeedGenerator appropriate for the given parameters + * @throws IllegalArgumentException when {@code parameters} is null + */ + public FeedGenerator resolve(Map parameterMap) { + Assert.notNull(parameterMap, "'parameters' cannot be null"); + Assert.isTrue(parameterMap.containsKey(REQUIRED_PARAMETER), format("Parameter map must contain key '%s'", REQUIRED_PARAMETER)); + String generatorName = parameterMap.get(REQUIRED_PARAMETER)[0]; + FeedGeneratorFactory feedGeneratorFactory = feedGeneratorFactories.get(generatorName); + try { + return feedGeneratorFactory.build(parameterMap); + } catch (Exception e) { + String message = format("Failed to construct FeedGenerator for name '%s' using FeedGeneratorFactory '%s'", + generatorName, (feedGeneratorFactory == null ? "not found" : feedGeneratorFactory.getClass().getName())); + throw new FeedGeneratorConstructionException(message, e); + } + } + + /** + * Register a FeedGeneratorFactory of the given class with the given name. + * + * @param name the name of the FeedGenerator to be build by the FeedGeneratorFactory registered + * @param feedGeneratoryFactory the FeedGeneratorFactory used to construct the FeedGenerator + * @throws IllegalArgumentException when {@code name} or {@code feedGeneratorFactoryClass} is null + */ + public void registerFeedGeneratorFactory(String name, FeedGeneratorFactory feedGeneratoryFactory) { + Assert.notNull(name, "'name' cannot be null"); + Assert.notNull(feedGeneratoryFactory, "'feedGeneratoryFactory' cannot be null"); + if (feedGeneratorFactories.containsKey(name)) { + throw new IllegalArgumentException(format("FeedGeneratorFactory for name '%s' has previously been registered", name)); + } + feedGeneratorFactories.put(name, feedGeneratoryFactory); + } +} Index: src/main/resources/mgnl-bootstrap/rssaggregator/config.modules.rssaggregator.config.xml =================================================================== --- src/main/resources/mgnl-bootstrap/rssaggregator/config.modules.rssaggregator.config.xml Sun Feb 22 20:52:26 CET 2009 +++ src/main/resources/mgnl-bootstrap/rssaggregator/config.modules.rssaggregator.config.xml Sun Feb 22 20:52:26 CET 2009 @@ -0,0 +1,83 @@ + + + + mgnl:content + + + mix:lockable + + + 261c4230-9aae-4f21-a64f-fd3b6aa3d6bf + + + + mgnl:metaData + + + superuser + + + 2009-02-22T19:15:08.937+01:00 + + + 2009-02-22T19:15:15.234+01:00 + + + + + mgnl:contentNode + + + mix:lockable + + + 4668e620-9567-4453-bc4e-ddf067b9c21c + + + + mgnl:metaData + + + superuser + + + 2009-02-22T19:15:32.718+01:00 + + + 2009-02-22T19:15:37.125+01:00 + + + + + mgnl:contentNode + + + mix:lockable + + + cc0d49b4-afa4-466a-ab4b-008de3bb9e7e + + + info.magnolia.module.rssaggregator.generator.RSSModuleFeedGeneratorFactory + + + + mgnl:metaData + + + superuser + + + 2009-02-22T19:15:45.140+01:00 + + + 2009-02-22T19:32:11.921+01:00 + + + + + Index: src/main/java/info/magnolia/module/rssaggregator/util/DataAccessException.java =================================================================== --- src/main/java/info/magnolia/module/rssaggregator/util/DataAccessException.java Sat Feb 21 20:58:09 CET 2009 +++ src/main/java/info/magnolia/module/rssaggregator/util/DataAccessException.java Sat Feb 21 20:58:09 CET 2009 @@ -0,0 +1,51 @@ +/** + * This file Copyright (c) 2008-2009 Magnolia International + * Ltd. (http://www.magnolia-cms.com). All rights reserved. + * + * + * This file is dual-licensed under both the Magnolia + * Network Agreement and the GNU General Public License. + * You may elect to use one or the other of these licenses. + * + * This file is distributed in the hope that it will be + * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the + * implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT. + * Redistribution, except as permitted by whichever of the GPL + * or MNA you select, is prohibited. + * + * 1. For the GPL license (GPL), you can redistribute and/or + * modify this file under the terms of the GNU General + * Public License, Version 3, as published by the Free Software + * Foundation. You should have received a copy of the GNU + * General Public License, Version 3 along with this program; + * if not, write to the Free Software Foundation, Inc., 51 + * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * 2. For the Magnolia Network Agreement (MNA), this file + * and the accompanying materials are made available under the + * terms of the MNA which accompanies this distribution, and + * is available at http://www.magnolia-cms.com/mna.html + * + * Any modifications to this file must keep this entire header + * intact. + * + */ +package info.magnolia.module.rssaggregator.util; + +/** + * Thrown when a data access exception occurs. + * + * @author Rob van der Linden Vooren + */ +public class DataAccessException extends RuntimeException { + + public DataAccessException(String message) { + super(message); + } + + public DataAccessException(String message, Throwable cause) { + super(message, cause); + } + +} Index: src/main/java/info/magnolia/module/rssaggregator/generator/FeedGeneratorFactory.java =================================================================== --- src/main/java/info/magnolia/module/rssaggregator/generator/FeedGeneratorFactory.java Sun Feb 22 11:54:58 CET 2009 +++ src/main/java/info/magnolia/module/rssaggregator/generator/FeedGeneratorFactory.java Sun Feb 22 11:54:58 CET 2009 @@ -0,0 +1,57 @@ +/** + * This file Copyright (c) 2008-2009 Magnolia International + * Ltd. (http://www.magnolia-cms.com). All rights reserved. + * + * + * This file is dual-licensed under both the Magnolia + * Network Agreement and the GNU General Public License. + * You may elect to use one or the other of these licenses. + * + * This file is distributed in the hope that it will be + * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the + * implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT. + * Redistribution, except as permitted by whichever of the GPL + * or MNA you select, is prohibited. + * + * 1. For the GPL license (GPL), you can redistribute and/or + * modify this file under the terms of the GNU General + * Public License, Version 3, as published by the Free Software + * Foundation. You should have received a copy of the GNU + * General Public License, Version 3 along with this program; + * if not, write to the Free Software Foundation, Inc., 51 + * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * 2. For the Magnolia Network Agreement (MNA), this file + * and the accompanying materials are made available under the + * terms of the MNA which accompanies this distribution, and + * is available at http://www.magnolia-cms.com/mna.html + * + * Any modifications to this file must keep this entire header + * intact. + * + */ +package info.magnolia.module.rssaggregator.generator; + +import java.util.Hashtable; +import java.util.Map; + +/** + * Strategy interface for constructing a {@link FeedGenerator FeedGenerator} based on a {@link Hashtable parameter + * map}. + * + * @author Rob van der Linden Vooren + * @see FeedGenerator + */ +public interface FeedGeneratorFactory { + + /** + * Construct a FeedGenerator based on the given {@code parameters}. Implementations are expected to document how + * provided parameters influence the FeedGenerator instance that is returned. + * + * @param parameters the parameters for FeedGenerator construction + * @return the constructed FeedGenerator + */ + FeedGenerator build(Map parameters); + +} Index: src/main/java/info/magnolia/module/rssaggregator/generator/Feed.java =================================================================== --- src/main/java/info/magnolia/module/rssaggregator/generator/Feed.java Sun Feb 22 21:15:47 CET 2009 +++ src/main/java/info/magnolia/module/rssaggregator/generator/Feed.java Sun Feb 22 21:15:47 CET 2009 @@ -0,0 +1,78 @@ +/** + * This file Copyright (c) 2008-2009 Magnolia International + * Ltd. (http://www.magnolia-cms.com). All rights reserved. + * + * + * This file is dual-licensed under both the Magnolia + * Network Agreement and the GNU General Public License. + * You may elect to use one or the other of these licenses. + * + * This file is distributed in the hope that it will be + * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the + * implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT. + * Redistribution, except as permitted by whichever of the GPL + * or MNA you select, is prohibited. + * + * 1. For the GPL license (GPL), you can redistribute and/or + * modify this file under the terms of the GNU General + * Public License, Version 3, as published by the Free Software + * Foundation. You should have received a copy of the GNU + * General Public License, Version 3 along with this program; + * if not, write to the Free Software Foundation, Inc., 51 + * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * 2. For the Magnolia Network Agreement (MNA), this file + * and the accompanying materials are made available under the + * terms of the MNA which accompanies this distribution, and + * is available at http://www.magnolia-cms.com/mna.html + * + * Any modifications to this file must keep this entire header + * intact. + * + */ +package info.magnolia.module.rssaggregator.generator; + +import info.magnolia.module.rssaggregator.util.Assert; + +/** + * A Feed. + * + * @author Rob van der Linden Vooren + */ +public class Feed { + + private final String xml; + private final String characterEncoding; + + /** + * Construct a new Feed with the given xml and characterEncoding. + * + * @param xml the xml of this feed (must not be null) + * @param characterEncoding the character encoding of this feed (must not be null) + */ + public Feed(String xml, String characterEncoding) { + Assert.notNull(xml, "'xml' must not be null"); + Assert.notNull(characterEncoding, "'characterEncoding' must not be null"); + this.xml = xml; + this.characterEncoding = characterEncoding; + } + + /** + * Return the xml of this feed. + * + * @return the xml of this feed + */ + public String getXml() { + return xml; + } + + /** + * Return the character encoding of this feed. + * + * @return the character encoding of this feed + */ + public String getCharacterEncoding() { + return characterEncoding; + } +} Index: src/main/java/info/magnolia/module/rssaggregator/importhandler/RSSFeedImportHandler.java =================================================================== --- src/main/java/info/magnolia/module/rssaggregator/importhandler/RSSFeedImportHandler.java (revision 22801) +++ src/main/java/info/magnolia/module/rssaggregator/importhandler/RSSFeedImportHandler.java Sun Feb 22 19:40:14 CET 2009 @@ -50,19 +50,12 @@ import javax.jcr.RepositoryException; import static java.lang.String.*; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.util.*; /** - * ImportHandler capable of importing RSS and Atom feeds over http for aggregate feeds defined in RSS Aggregator - * module. - *

- * Allows optional configuration with a {@link RSSFeedFetcher} implementation of choice, by means of configuring the - * content node: + * ImportHandler capable of importing RSS and Atom feeds over http for aggregate feeds defined in RSS Aggregator module. + *

Allows optional configuration with a {@link RSSFeedFetcher} implementation of choice, by means of configuring + * the content node: *

  * /data/rssaggregator/
  * 
@@ -74,8 +67,9 @@ public class RSSFeedImportHandler extends ImportHandler { private static final String CONTENTTYPE_RSSAGGREGATOR = "RssAggregator"; - private static final Logger logger = LoggerFactory.getLogger(RSSFeedImportHandler.class); + private Logger logger = LoggerFactory.getLogger(getClass()); + private RSSFeedFetcher feedFetcher; private AggregateFeedContentMapper aggregateFeedMapper; private FilterPredicateContentMapper filterPredicateMapper; @@ -92,9 +86,9 @@ protected synchronized void checkPreConditions() throws ImportException { super.checkPreConditions(); if (feedFetcher == null) { - feedFetcher = (RSSFeedFetcher) FactoryUtil.newInstance(RSSFeedFetcher.class); + feedFetcher = (RSSFeedFetcher) FactoryUtil.newInstance(SimpleRSSFeedFetcher.class); } - logger.debug(format("Using feed fetcher '%s'", feedFetcher.getClass().getName())); + logger.info(format("Using feed fetcher '%s'", feedFetcher.getClass().getName())); } @SuppressWarnings("unchecked") @@ -102,11 +96,18 @@ try { Set aggregateFeeds = loadAggregates(parentNode); if (!aggregateFeeds.isEmpty()) { - Set fetchedAggregateFeeds = feedFetcher.fetchAggregate(aggregateFeeds); + if (logger.isInfoEnabled()) { + logger.info(format("Fetching %s aggregate feeds (%s channels)", aggregateFeeds.size(), + countChannels(aggregateFeeds))); + } + Set fetchedAggregateFeeds = feedFetcher.fetchAggregateFeeds(aggregateFeeds); Set newAggregateContentUUIDs = saveAggregates(fetchedAggregateFeeds, parentNode); newContentUUIDs.addAll(newAggregateContentUUIDs); parentNode.save(); + if (logger.isInfoEnabled()) { + logger.info(format("Fetching complete")); - } + } + } return newContentUUIDs; } catch (Exception e) { String message = format("Failed to execute import for target '%s', parent node '%s'", target, parentNode); @@ -116,6 +117,14 @@ // Helper methods + private int countChannels(Set aggregateFeeds) { + int channelCount = 0; + for (AggregateFeed aggregateFeed : aggregateFeeds) { + channelCount += aggregateFeed.getChannels().size(); + } + return channelCount; + } + /** * Load the {@link AggregateFeed aggregate feed} definitions and their {@link FeedChannel feed channels} from the * Content Repository. @@ -141,7 +150,7 @@ * {@link AggregateFilter} defined, feed entries must pass the filter before they will be actually saved in the * Content Repository. * - * @param parentNode the parent content node of the aggregate feeds content to save + * @param parentNode the parent content node of the aggregate feeds content to save * @param aggregateFeeds the aggregate feeds to save * @return a set of UUIDs of the newly created aggregate content nodes * @throws RepositoryException when an exception occurs accessing the Content Repository @@ -166,7 +175,7 @@ * Load a single aggregate content node from the given parentNode with the given * aggregateName. If no such aggregate could be found, null is returned. * - * @param parentNode the parentNode to load the node from + * @param parentNode the parentNode to load the node from * @param aggregateNodeName the name of the aggregate content node to load * @return the aggregate content node, or null if no such node was found * @throws IllegalStateException when multiple aggregate content nodes with the same name are found @@ -190,8 +199,8 @@ * testability. * * @param contentNode the contentNode to (create if non-existant and then) get - * @param name the name of the node + * @param name the name of the node - * @param itemType the type of the content node + * @param itemType the type of the content node * @return the created content node * @throws RepositoryException when an exception occurs accessing the Content Repository */ @@ -229,8 +238,8 @@ * Save the {@link SyndFeed#getEntries() entries} contained {@link FeedChannel#feed in} the given {@link * FeedChannel} that pass the given {@link AggregateFilter} in the provided dataNode. * - * @param dataNode the content node to store the feed content under + * @param dataNode the content node to store the feed content under - * @param feedChannel the feed channel to save + * @param feedChannel the feed channel to save * @param aggregateFilter the aggregate filter to apply to entries in the feed channel * @throws RepositoryException when an exception occurs accessing the Content Repository */ @@ -251,7 +260,7 @@ /** * Recreate the feed channel content node the given feed channel in the Content Repository. * - * @param dataNode the node to store the feed channel under + * @param dataNode the node to store the feed channel under * @param feedChannel the feed channel to recreate * @return the created feed channel content node * @throws RepositoryException when an exception occurs accessing the Content Repository @@ -275,8 +284,8 @@ * Create a feed channel entry node under the given channelNodewith the given entryNamefor * the given entry. * - * @param entry the feed channel entry to save + * @param entry the feed channel entry to save - * @param nodeName the name of the feed channel entry node to create + * @param nodeName the name of the feed channel entry node to create * @param channelNode the feed channel content node to create the feed channel entry under * @throws RepositoryException when an exception occurs accessing the Content Repository */ @@ -310,8 +319,7 @@ // Getters & setters - /** for testing */ - protected void setFeedFetcher(RSSFeedFetcher rssFeedFetcher) { + public void setFeedFetcher(RSSFeedFetcher rssFeedFetcher) { Assert.notNull(rssFeedFetcher, "'rssFeedFetcher' must not be null"); this.feedFetcher = rssFeedFetcher; } @@ -328,4 +336,9 @@ this.filterPredicateMapper = filterPredicateMapper; } + /** for testing */ + protected void setLogger(Logger logger) { + Assert.notNull(logger, "'logger' must not be null"); + this.logger = logger; -} \ No newline at end of file + } +} Index: src/main/java/info/magnolia/module/rssaggregator/util/MagnoliaQueryOperations.java =================================================================== --- src/main/java/info/magnolia/module/rssaggregator/util/MagnoliaQueryOperations.java Sun Feb 22 21:12:18 CET 2009 +++ src/main/java/info/magnolia/module/rssaggregator/util/MagnoliaQueryOperations.java Sun Feb 22 21:12:18 CET 2009 @@ -0,0 +1,120 @@ +/** + * This file Copyright (c) 2008-2009 Magnolia International + * Ltd. (http://www.magnolia-cms.com). All rights reserved. + * + * + * This file is dual-licensed under both the Magnolia + * Network Agreement and the GNU General Public License. + * You may elect to use one or the other of these licenses. + * + * This file is distributed in the hope that it will be + * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the + * implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT. + * Redistribution, except as permitted by whichever of the GPL + * or MNA you select, is prohibited. + * + * 1. For the GPL license (GPL), you can redistribute and/or + * modify this file under the terms of the GNU General + * Public License, Version 3, as published by the Free Software + * Foundation. You should have received a copy of the GNU + * General Public License, Version 3 along with this program; + * if not, write to the Free Software Foundation, Inc., 51 + * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * 2. For the Magnolia Network Agreement (MNA), this file + * and the accompanying materials are made available under the + * terms of the MNA which accompanies this distribution, and + * is available at http://www.magnolia-cms.com/mna.html + * + * Any modifications to this file must keep this entire header + * intact. + * + */ +package info.magnolia.module.rssaggregator.util; + +import info.magnolia.cms.core.Content; +import info.magnolia.cms.core.ItemType; +import info.magnolia.cms.core.search.Query; + +import java.util.List; + +/** + * Specifies a basic set of Magnolia query operations. Implemented by {@link MagnoliaTemplate}. Not often used directly, + * but a useful option to enhance testability, as it can easily be mocked or stubbed. + * + * @author Rob van der Linden Vooren + * @see MagnoliaTemplate + */ +public interface MagnoliaQueryOperations { + + /** + * Execute a query for a single or {@code null} result object, for the given query.

This method is useful for + * running queries with a known outcome. The query is expected to be a single result query; the returned result will + * be directly mapped to the corresponding object type. + * + * @param repository the repository to execute query against + * @param query query to execute + * @param language query language (either {@link Query#XPATH XPATH} or {@link Query#SQL SQL}). + * @param type the type of the item to query for + * @param mapper the mapper used + * @return the result object of the required type, or {@code null} in case of a {@code null} query + * @throws IncorrectResultSizeDataAccessException + * if the query returns more than one results + * @throws DataAccessException if there is any problem executing the query + */ + T queryForObject(String repository, String query, String language, ItemType type, ContentMapper mapper) throws DataAccessException; + + /** + * Execute a query for a result list of type T, for the given query.

+ * + * @param repository the repository to execute query against + * @param query query to execute + * @param language query language (either {@link Query#XPATH XPATH} or {@link Query#SQL SQL}). + * @param type the type of the item to query for + * @param mapper the mapper used + * @return the result object of the required type, or {@code null} in case of a {@code null} query + * @throws DataAccessException if there is any problem executing the query + */ + List queryForList(String repository, String query, String language, ItemType type, ContentMapper mapper) throws DataAccessException; + + /** + * Execute an XPath query for a single or {@code null} result object, for the given query.

This method is + * useful for running queries with a known outcome. The query is expected to be a single result query; the returned + * result will be directly mapped to the corresponding object type. + * + * @param repository the repository to execute query against + * @param query query to execute + * @param type the type of the item to query for + * @param mapper the mapper used + * @return the result object of the required type, or {@code null} in case of a {@code null} query + * @throws IncorrectResultSizeDataAccessException + * if the query returns more than one results + * @throws DataAccessException if there is any problem executing the query + */ + T xpathQueryForObject(String repository, String query, ItemType type, ContentMapper mapper) throws DataAccessException; + + /** + * Execute an XPath query for a result list of type T, for the given query. + * + * @param repository the repository to execute query against + * @param query query to execute + * @param type the type of the item to query for + * @param mapper the mapper used + * @return the result object of the required type, or {@code null} in case of a {@code null} Xpath query + * @throws DataAccessException if there is any problem executing the query + */ + List xpathQueryForList(String repository, String query, ItemType type, ContentMapper mapper) throws DataAccessException; + + /** + * Execute an XPath query for a Content node. + * + * @param repository the repository to execute query against + * @param query query to execute + * @param type the type of the item to query for + * @return the result Content node, or {@code null} if no such node exists + * @throws DataAccessException if there is any problem executing the query + */ + Content xpathQueryForContent(String repository, String query, ItemType type) throws DataAccessException; + +} Index: src/main/java/info/magnolia/module/rssaggregator/importhandler/SimpleRSSFeedFetcher.java =================================================================== --- src/main/java/info/magnolia/module/rssaggregator/importhandler/SimpleRSSFeedFetcher.java (revision 22801) +++ src/main/java/info/magnolia/module/rssaggregator/importhandler/SimpleRSSFeedFetcher.java Sun Feb 22 21:19:17 CET 2009 @@ -46,31 +46,26 @@ /** * A simple single threaded {@link RSSFeedFetcher} for feed retrieval http that follows redirects and - * supports conditional gets. - *

- * Will attempt to fetch all {@link FeedChannel feed channels} defined in the provided {@link AggregateFeed aggregate - * feeds}. Failures to fetch a feed result for a given feed channel are logged at ERROR level. - *

- * Because of the single threaded nature of this implementation, it is suited for situations where the number feed - * channels to retrieve is not large and the time window between update checks is not small. - *

- * For more demanding situations, use a more performant RSSFeedFetcher implementation. + * supports conditional gets.

Will attempt to fetch all {@link FeedChannel feed channels} defined in the provided + * {@link AggregateFeed aggregate feeds}. Failures to fetch a feed result for a given feed channel are logged at ERROR + * level.

Because of the single threaded nature of this implementation, it is suited for situations where the + * number feed channels to retrieve is small and the time window between update checks is large enough.

For more + * demanding situations, use a more performant RSSFeedFetcher implementation. * * @author Rob van der Linden Vooren * @see RSSFeedFetcher - * @see */ public class SimpleRSSFeedFetcher implements RSSFeedFetcher { private final FeedFetcherCache feedInfoCache; - private Logger logger = LoggerFactory.getLogger(SimpleRSSFeedFetcher.class); + private Logger logger = LoggerFactory.getLogger(getClass()); public SimpleRSSFeedFetcher() { this.feedInfoCache = HashMapFeedInfoCache.getInstance(); } - public Set fetchAggregate(Set aggregateFeeds) { + public Set fetchAggregateFeeds(Set aggregateFeeds) { for (AggregateFeed aggregateFeed : aggregateFeeds) { for (FeedChannel channel : aggregateFeed.getChannels()) { if (logger.isDebugEnabled()) { @@ -111,5 +106,4 @@ protected void setLogger(Logger logger) { this.logger = logger; } - } Index: src/main/java/info/magnolia/module/rssaggregator/FeedGenerator.java =================================================================== --- src/main/java/info/magnolia/module/rssaggregator/FeedGenerator.java (revision 22801) +++ src/main/java/info/magnolia/module/rssaggregator/FeedGenerator.java (revision 22801) @@ -1,150 +0,0 @@ -/** - * This file Copyright (c) 2008-2009 Magnolia International - * Ltd. (http://www.magnolia-cms.com). All rights reserved. - * - * - * This file is dual-licensed under both the Magnolia - * Network Agreement and the GNU General Public License. - * You may elect to use one or the other of these licenses. - * - * This file is distributed in the hope that it will be - * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the - * implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT. - * Redistribution, except as permitted by whichever of the GPL - * or MNA you select, is prohibited. - * - * 1. For the GPL license (GPL), you can redistribute and/or - * modify this file under the terms of the GNU General - * Public License, Version 3, as published by the Free Software - * Foundation. You should have received a copy of the GNU - * General Public License, Version 3 along with this program; - * if not, write to the Free Software Foundation, Inc., 51 - * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * 2. For the Magnolia Network Agreement (MNA), this file - * and the accompanying materials are made available under the - * terms of the MNA which accompanies this distribution, and - * is available at http://www.magnolia-cms.com/mna.html - * - * Any modifications to this file must keep this entire header - * intact. - * - */ -package info.magnolia.module.rssaggregator; - -import com.sun.syndication.feed.synd.SyndCategory; -import com.sun.syndication.feed.synd.SyndCategoryImpl; -import com.sun.syndication.feed.synd.SyndContent; -import com.sun.syndication.feed.synd.SyndContentImpl; -import com.sun.syndication.feed.synd.SyndEntry; -import com.sun.syndication.feed.synd.SyndEntryImpl; -import com.sun.syndication.feed.synd.SyndFeed; -import com.sun.syndication.feed.synd.SyndFeedImpl; -import com.sun.syndication.io.FeedException; -import com.sun.syndication.io.SyndFeedOutput; - -import info.magnolia.cms.core.Content; -import info.magnolia.cms.core.HierarchyManager; -import info.magnolia.cms.core.ItemType; -import info.magnolia.cms.core.NodeData; -import info.magnolia.cms.core.search.Query; -import info.magnolia.cms.core.search.QueryManager; -import info.magnolia.cms.core.search.QueryResult; -import info.magnolia.context.MgnlContext; - -import java.io.IOException; -import java.io.Writer; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; - -/** - * Generator of the rss xml from combined feeds defined by this module. - * @author had - * - */ -public class FeedGenerator { - - private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(FeedGenerator.class); - - public void writeBlogRSS(String feedType, Writer writer, String feedPath) { - - Iterator blogEntries = null; - Content blogInfo = null; - - try { - - QueryManager qm = MgnlContext.getQueryManager("data"); - HierarchyManager hm = MgnlContext.getHierarchyManager("data"); - String queryString = "/jcr:root" + feedPath + "/data[1]/*/* order by @pubDate descending"; - log.debug(queryString); - Query q = qm.createQuery(queryString, "xpath"); - QueryResult res = q.execute(); - blogEntries = res.getContent(ItemType.CONTENTNODE.getSystemName()) - .iterator(); - blogInfo = hm.getContent(feedPath); - List entries = new ArrayList(); - while (blogEntries.hasNext()) { - Content blogEntry = (Content) blogEntries.next(); - entries.add(createEntry(blogEntry.getNodeData("title").getString(), - blogEntry.getNodeData("link").getString(), blogEntry - .getNodeData("pubDate").getDate(), blogEntry - .getNodeData("description").getString(), - blogEntry.getContent("categories") - .getNodeDataCollection(), blogEntry - .getNodeData("author").getString())); - } - - this.doSyndication(feedType, blogInfo.getNodeData("title").getString(), blogInfo.getNodeData("link").getString(), - blogInfo.getNodeData("description").getString(), entries, writer); - - } catch (Exception e) { - log.error(e.getMessage(), e); - } - } - - private SyndEntry createEntry(String title, String link, Calendar date, - String blogContent, Collection cat, String author) { - - SyndEntry entry = new SyndEntryImpl(); - entry.setAuthor(author); - entry.setTitle(title); - entry.setLink(link); - if (date != null) { - entry.setPublishedDate(date.getTime()); - } - SyndContent description = new SyndContentImpl(); - description.setType("text/html"); - description.setValue(blogContent); - entry.setDescription(description); - List categories = new ArrayList(); - for (Iterator iter = cat.iterator(); iter.hasNext();) { - SyndCategory category = new SyndCategoryImpl(); - category.setName(((NodeData) iter.next()).getString()); - categories.add(category); - } - entry.setCategories(categories); - return entry; - - } - - private void doSyndication(String feedType, String title, String link, String description, List entries, Writer writer) throws IOException, FeedException { - - final SyndFeed feed = new SyndFeedImpl(); - feed.setFeedType(feedType); - - feed.setTitle(title); - feed.setLink(link); - feed.setDescription(description); - - feed.setEntries(entries); - - final SyndFeedOutput output = new SyndFeedOutput(); - output.output(feed, writer); - - } - -} Index: src/main/java/info/magnolia/module/rssaggregator/util/IncorrectResultSizeDataAccessException.java =================================================================== --- src/main/java/info/magnolia/module/rssaggregator/util/IncorrectResultSizeDataAccessException.java Sat Feb 21 20:58:09 CET 2009 +++ src/main/java/info/magnolia/module/rssaggregator/util/IncorrectResultSizeDataAccessException.java Sat Feb 21 20:58:09 CET 2009 @@ -0,0 +1,78 @@ +/** + * This file Copyright (c) 2008-2009 Magnolia International + * Ltd. (http://www.magnolia-cms.com). All rights reserved. + * + * + * This file is dual-licensed under both the Magnolia + * Network Agreement and the GNU General Public License. + * You may elect to use one or the other of these licenses. + * + * This file is distributed in the hope that it will be + * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the + * implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT. + * Redistribution, except as permitted by whichever of the GPL + * or MNA you select, is prohibited. + * + * 1. For the GPL license (GPL), you can redistribute and/or + * modify this file under the terms of the GNU General + * Public License, Version 3, as published by the Free Software + * Foundation. You should have received a copy of the GNU + * General Public License, Version 3 along with this program; + * if not, write to the Free Software Foundation, Inc., 51 + * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * 2. For the Magnolia Network Agreement (MNA), this file + * and the accompanying materials are made available under the + * terms of the MNA which accompanies this distribution, and + * is available at http://www.magnolia-cms.com/mna.html + * + * Any modifications to this file must keep this entire header + * intact. + * + */ +package info.magnolia.module.rssaggregator.util; + +/** + * Data access exception thrown when a result was not of the expected size, for example when expecting a single content + * node but getting 0 or more than 1. + * + * @author Rob van der Linden Vooren + */ +public class IncorrectResultSizeDataAccessException extends DataAccessException { + + private int expectedSize; + private int actualSize; + + /** + * Construct a new IncorrectResultSizeDataAccessException. + * + * @param expectedSize the expected result size + */ + public IncorrectResultSizeDataAccessException(int expectedSize) { + super("Incorrect result size: expected " + expectedSize); + this.expectedSize = expectedSize; + this.actualSize = -1; + } + + /** + * Construct a new IncorrectResultSizeDataAccessException. + * + * @param expectedSize the expected result size + * @param actualSize the actual result size (or -1 if unknown) + */ + public IncorrectResultSizeDataAccessException(int expectedSize, int actualSize) { + super("Incorrect result size: expected " + expectedSize + ", actual " + actualSize); + this.expectedSize = expectedSize; + this.actualSize = actualSize; + } + + public int getExpectedSize() { + return expectedSize; + } + + public int getActualSize() { + return actualSize; + } + +} Index: src/test/java/info/magnolia/module/rssaggregator/generator/FeedGeneratorResolverTest.java =================================================================== --- src/test/java/info/magnolia/module/rssaggregator/generator/FeedGeneratorResolverTest.java Sun Feb 22 21:10:48 CET 2009 +++ src/test/java/info/magnolia/module/rssaggregator/generator/FeedGeneratorResolverTest.java Sun Feb 22 21:10:48 CET 2009 @@ -0,0 +1,106 @@ +/** + * This file Copyright (c) 2008-2009 Magnolia International + * Ltd. (http://www.magnolia-cms.com). All rights reserved. + * + * + * This file is dual-licensed under both the Magnolia + * Network Agreement and the GNU General Public License. + * You may elect to use one or the other of these licenses. + * + * This file is distributed in the hope that it will be + * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the + * implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT. + * Redistribution, except as permitted by whichever of the GPL + * or MNA you select, is prohibited. + * + * 1. For the GPL license (GPL), you can redistribute and/or + * modify this file under the terms of the GNU General + * Public License, Version 3, as published by the Free Software + * Foundation. You should have received a copy of the GNU + * General Public License, Version 3 along with this program; + * if not, write to the Free Software Foundation, Inc., 51 + * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * 2. For the Magnolia Network Agreement (MNA), this file + * and the accompanying materials are made available under the + * terms of the MNA which accompanies this distribution, and + * is available at http://www.magnolia-cms.com/mna.html + * + * Any modifications to this file must keep this entire header + * intact. + * + */ +package info.magnolia.module.rssaggregator.generator; + +import com.sun.syndication.feed.synd.SyndEntry; +import com.sun.syndication.feed.synd.SyndFeed; +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Test; + +import java.util.Hashtable; +import java.util.List; +import java.util.Map; + +/** + * Tests {@link FeedGeneratorResolver}. + * + * @author Rob van der Linden Vooren + */ +public class FeedGeneratorResolverTest { + + private FeedGeneratorResolver resolver; + private Map parameters; + + @Before + public void before() { + resolver = new FeedGeneratorResolver(); + parameters = new Hashtable(); + } + + @Test + public void testResolve() { + resolver.registerFeedGeneratorFactory("testGenerator", new TestableFeedGeneratorFactory()); + + parameters.put("generatorName", new String[]{"testGenerator"}); + + FeedGenerator generator = resolver.resolve(parameters); + assertNotNull(generator); + assertTrue(generator instanceof TestableFeedGenerator); + } + + @Test + public void testResolve_throwsExceptionWhenUnresolvable() { + parameters.put("generatorName", new String[]{"no-factory-registered-for-this-generator"}); + + try { + resolver.resolve(parameters); + fail("FeedGeneratorConstructionException expected"); + } catch (FeedGeneratorConstructionException exception) { + assertNotNull(exception.getCause()); + assertNotNull(exception.getMessage()); + } + } + + private static class TestableFeedGeneratorFactory implements FeedGeneratorFactory { + + public TestableFeedGeneratorFactory() { + } + + public AbstractSyndFeedGenerator build(Map parameters) { + return new TestableFeedGenerator(); + } + } + + private static class TestableFeedGenerator extends AbstractSyndFeedGenerator { + + public void setFeedInfo(SyndFeed feed) { + throw new UnsupportedOperationException("Not implemented"); + } + + public List loadFeedEntries() { + throw new UnsupportedOperationException("Not implemented"); + } + } +} Index: src/main/java/info/magnolia/module/rssaggregator/generator/RSSModuleFeedGenerator.java =================================================================== --- src/main/java/info/magnolia/module/rssaggregator/generator/RSSModuleFeedGenerator.java Sun Feb 22 19:02:06 CET 2009 +++ src/main/java/info/magnolia/module/rssaggregator/generator/RSSModuleFeedGenerator.java Sun Feb 22 19:02:06 CET 2009 @@ -0,0 +1,118 @@ +/** + * This file Copyright (c) 2008-2009 Magnolia International + * Ltd. (http://www.magnolia-cms.com). All rights reserved. + * + * + * This file is dual-licensed under both the Magnolia + * Network Agreement and the GNU General Public License. + * You may elect to use one or the other of these licenses. + * + * This file is distributed in the hope that it will be + * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the + * implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT. + * Redistribution, except as permitted by whichever of the GPL + * or MNA you select, is prohibited. + * + * 1. For the GPL license (GPL), you can redistribute and/or + * modify this file under the terms of the GNU General + * Public License, Version 3, as published by the Free Software + * Foundation. You should have received a copy of the GNU + * General Public License, Version 3 along with this program; + * if not, write to the Free Software Foundation, Inc., 51 + * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * 2. For the Magnolia Network Agreement (MNA), this file + * and the accompanying materials are made available under the + * terms of the MNA which accompanies this distribution, and + * is available at http://www.magnolia-cms.com/mna.html + * + * Any modifications to this file must keep this entire header + * intact. + * + */ +package info.magnolia.module.rssaggregator.generator; + +import com.sun.syndication.feed.synd.*; +import info.magnolia.cms.core.Content; +import static info.magnolia.cms.core.ItemType.*; +import info.magnolia.cms.core.NodeData; +import info.magnolia.module.rssaggregator.util.Assert; +import info.magnolia.module.rssaggregator.util.ContentMapper; +import info.magnolia.module.rssaggregator.util.MagnoliaTemplate; + +import javax.jcr.RepositoryException; +import static java.lang.String.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * Generates a {@link SyndFeed} based on aggregate feeds defined via the RSS Aggregator Module. + * + * @author Rob van der Linden Vooren + */ +public class RSSModuleFeedGenerator extends AbstractSyndFeedGenerator { + + private static final String REPOSITORY = "data"; + private static final ContentMapper MAPPER = new FeedEntryMapper(); + + private MagnoliaTemplate magnoliaTemplate; + + private final String feedPath; + + /** + * Construct a new RSSModuleSyndFeedGenerator that generates a SyndFeed for the aggregate feed defined via the RSS + * Aggregator Module at the given {@code feedPath}. + * + * @param feedPath the path of the aggregate feed in the RSS Aggregator module (eg. "/rssaggregator/blogsaggregate") + * @throws IllegalArgumentException when {@code feedPath} is null + */ + public RSSModuleFeedGenerator(String feedPath) { + Assert.notNull(feedPath, "'feedPath' must not be null"); + this.feedPath = feedPath; + this.magnoliaTemplate = new MagnoliaTemplate(); + } + + public void setFeedInfo(SyndFeed feed) { + // Ideally retrieve this from the aggregate node definition. However the RSS Aggregate creation dialog does not + // provide for this information (yet). + feed.setTitle("My Aggregate Feed"); + feed.setLink("http://mydomain.com/rss/feed.xml"); + feed.setDescription("My Latest Entries."); + } + + public List loadFeedEntries() { + String entriesQuery = format("/jcr:root%s/data[1]/*/* order by @pubDate descending", feedPath); + return magnoliaTemplate.xpathQueryForList(REPOSITORY, entriesQuery, CONTENTNODE, MAPPER); + } + + private static class FeedEntryMapper implements ContentMapper { + + public SyndEntry map(Content content) throws RepositoryException { + SyndEntry entry = new SyndEntryImpl(); + entry.setAuthor(content.getNodeData("author").getString()); + entry.setTitle(content.getNodeData("title").getString()); + entry.setLink(content.getNodeData("link").getString()); + if (content.getNodeData("pubDate").getDate() != null) { + entry.setPublishedDate(content.getNodeData("pubDate").getDate().getTime()); + } + SyndContent description = new SyndContentImpl(); + description.setType("text/html"); + description.setValue(content.getNodeData("description").getString()); + entry.setDescription(description); + List categories = new ArrayList(); + @SuppressWarnings("unchecked") + Collection categoriesNodeData = content.getContent("categories").getNodeDataCollection(); + for (NodeData nodeData : categoriesNodeData) { + SyndCategory category = new SyndCategoryImpl(); + category.setName(nodeData.getString()); + categories.add(category); + } + entry.setCategories(categories); + return entry; + } + + } + +} Index: src/main/java/info/magnolia/module/rssaggregator/importhandler/FeedChannelContentMapper.java =================================================================== --- src/main/java/info/magnolia/module/rssaggregator/importhandler/FeedChannelContentMapper.java (revision 22801) +++ src/main/java/info/magnolia/module/rssaggregator/importhandler/FeedChannelContentMapper.java Sat Feb 21 20:58:09 CET 2009 @@ -34,9 +34,10 @@ package info.magnolia.module.rssaggregator.importhandler; import info.magnolia.cms.core.Content; +import info.magnolia.module.rssaggregator.util.ContentMapper; import javax.jcr.RepositoryException; -import static java.lang.String.*; +import static java.lang.String.format; /** * Maps a content node defining a feed channel to an actual {@link FeedChannel}. @@ -55,7 +56,7 @@ * @param feedChannelNode the content node defining a feed channel * @return a feed channel mapped from the given feedNode when mapped succesful, null if * the content node does not have link node data - * @throws RepositoryException when an exception occurs + * @throws RepositoryException when an exception occurs * @throws IllegalArgumentException when link or title node data are blank */ public FeedChannel map(Content feedChannelNode) throws RepositoryException { Index: src/test/java/info/magnolia/module/rssaggregator/importhandler/RSSFeedImportHandlerTest.java =================================================================== --- src/test/java/info/magnolia/module/rssaggregator/importhandler/RSSFeedImportHandlerTest.java (revision 22801) +++ src/test/java/info/magnolia/module/rssaggregator/importhandler/RSSFeedImportHandlerTest.java Sun Feb 22 19:40:45 CET 2009 @@ -48,6 +48,8 @@ import static org.junit.Assert.*; import org.junit.Before; import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.jcr.RepositoryException; import static java.util.Arrays.*; @@ -63,6 +65,8 @@ */ public class RSSFeedImportHandlerTest { + private Logger logger = LoggerFactory.getLogger(getClass()); + private RSSFeedImportHandler handler; private RSSFeedFetcher mockRSSFeedFetcher; private AggregateFeedContentMapper mockAggregateFeedMapper; @@ -83,6 +87,7 @@ RSSFeedImportHandler.class.getDeclaredMethod("saveAggregates", Set.class, Content.class) ); handler.setFeedFetcher(mockRSSFeedFetcher); + handler.setLogger(logger); ImportTarget dummyTarget = createMock(ImportTarget.class); Content mockParentNode = createStrictMock("mockParentNode", Content.class); @@ -90,7 +95,7 @@ Set expectedAggregateFeeds = asSet(new AggregateFeed("aggregate-1"), new AggregateFeed("aggregate-2")); expect(handler.loadAggregates(mockParentNode)).andReturn(expectedAggregateFeeds); - expect(mockRSSFeedFetcher.fetchAggregate(expectedAggregateFeeds)).andReturn(expectedAggregateFeeds); + expect(mockRSSFeedFetcher.fetchAggregateFeeds(expectedAggregateFeeds)).andReturn(expectedAggregateFeeds); expect(handler.saveAggregates(expectedAggregateFeeds, mockParentNode)).andReturn(expectedUUIDs); mockParentNode.save(); replay(handler, mockRSSFeedFetcher, mockParentNode); Index: src/main/java/info/magnolia/module/rssaggregator/importhandler/ContentMapper.java =================================================================== --- src/main/java/info/magnolia/module/rssaggregator/importhandler/ContentMapper.java (revision 22801) +++ src/main/java/info/magnolia/module/rssaggregator/importhandler/ContentMapper.java (revision 22801) @@ -1,56 +0,0 @@ -/** - * This file Copyright (c) 2008-2009 Magnolia International - * Ltd. (http://www.magnolia-cms.com). All rights reserved. - * - * - * This file is dual-licensed under both the Magnolia - * Network Agreement and the GNU General Public License. - * You may elect to use one or the other of these licenses. - * - * This file is distributed in the hope that it will be - * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the - * implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT. - * Redistribution, except as permitted by whichever of the GPL - * or MNA you select, is prohibited. - * - * 1. For the GPL license (GPL), you can redistribute and/or - * modify this file under the terms of the GNU General - * Public License, Version 3, as published by the Free Software - * Foundation. You should have received a copy of the GNU - * General Public License, Version 3 along with this program; - * if not, write to the Free Software Foundation, Inc., 51 - * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * 2. For the Magnolia Network Agreement (MNA), this file - * and the accompanying materials are made available under the - * terms of the MNA which accompanies this distribution, and - * is available at http://www.magnolia-cms.com/mna.html - * - * Any modifications to this file must keep this entire header - * intact. - * - */ -package info.magnolia.module.rssaggregator.importhandler; - -import info.magnolia.cms.core.Content; - -import javax.jcr.RepositoryException; - -/** - * Capable of mapping a {@link Content content} node {@link #map(Content) to} an object of type T. - * - * @author Rob van der Linden Vooren - */ -public interface ContentMapper { - - /** - * Map a content node that defines a type T to an object of that type. - * - * @param content the content node to map - * @return the result object of type T - * @throws RepositoryException when a problem occurred accessing the underlying repository - */ - T map(Content content) throws RepositoryException; - -} Index: src/main/java/info/magnolia/module/rssaggregator/generator/RSSModuleFeedGeneratorFactory.java =================================================================== --- src/main/java/info/magnolia/module/rssaggregator/generator/RSSModuleFeedGeneratorFactory.java Sun Feb 22 21:14:43 CET 2009 +++ src/main/java/info/magnolia/module/rssaggregator/generator/RSSModuleFeedGeneratorFactory.java Sun Feb 22 21:14:43 CET 2009 @@ -0,0 +1,67 @@ +/** + * This file Copyright (c) 2008-2009 Magnolia International + * Ltd. (http://www.magnolia-cms.com). All rights reserved. + * + * + * This file is dual-licensed under both the Magnolia + * Network Agreement and the GNU General Public License. + * You may elect to use one or the other of these licenses. + * + * This file is distributed in the hope that it will be + * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the + * implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT. + * Redistribution, except as permitted by whichever of the GPL + * or MNA you select, is prohibited. + * + * 1. For the GPL license (GPL), you can redistribute and/or + * modify this file under the terms of the GNU General + * Public License, Version 3, as published by the Free Software + * Foundation. You should have received a copy of the GNU + * General Public License, Version 3 along with this program; + * if not, write to the Free Software Foundation, Inc., 51 + * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * 2. For the Magnolia Network Agreement (MNA), this file + * and the accompanying materials are made available under the + * terms of the MNA which accompanies this distribution, and + * is available at http://www.magnolia-cms.com/mna.html + * + * Any modifications to this file must keep this entire header + * intact. + * + */ +package info.magnolia.module.rssaggregator.generator; + +import info.magnolia.module.rssaggregator.util.Assert; + +import static java.lang.String.*; +import java.util.Map; + +/** + * Constructs a configured RSSModuleSyndFeedGenerator based on given parameters. For details on required parameters see + * {@link #build(Map)}. + * + * @author Rob van der Linden Vooren + * @see FeedGeneratorFactory + * @see AbstractSyndFeedGenerator + */ +public class RSSModuleFeedGeneratorFactory implements FeedGeneratorFactory { + + private static final String PARAMETER_FEEDPATH = "feedPath"; + + /** + * Construct a new configured RSSModuleSyndFeedGenerator based on the given {@code parameters}. The only required + * parameter to construct this generator is "{@value #PARAMETER_FEEDPATH}"; this is expected to be the path to a + * feed aggregate as defined in the RSS Aggregator Module. (e.g. "/rssaggregator/blogsaggregate") + * + * @param parameters the parameters to SyndFeedGenerator construction + * @return the configured RSSModuleSyndFeedGenerator + */ + public AbstractSyndFeedGenerator build(Map parameters) { + Assert.isTrue(parameters.containsKey(PARAMETER_FEEDPATH), format("Parameters must contain key '%s'", PARAMETER_FEEDPATH)); + String feedPath = parameters.get(PARAMETER_FEEDPATH)[0]; + return new RSSModuleFeedGenerator(feedPath); + } + +} Index: src/main/java/info/magnolia/module/rssaggregator/util/ContentMapper.java =================================================================== --- src/main/java/info/magnolia/module/rssaggregator/util/ContentMapper.java Sat Feb 21 20:58:08 CET 2009 +++ src/main/java/info/magnolia/module/rssaggregator/util/ContentMapper.java Sat Feb 21 20:58:08 CET 2009 @@ -0,0 +1,56 @@ +/** + * This file Copyright (c) 2008-2009 Magnolia International + * Ltd. (http://www.magnolia-cms.com). All rights reserved. + * + * + * This file is dual-licensed under both the Magnolia + * Network Agreement and the GNU General Public License. + * You may elect to use one or the other of these licenses. + * + * This file is distributed in the hope that it will be + * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the + * implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT. + * Redistribution, except as permitted by whichever of the GPL + * or MNA you select, is prohibited. + * + * 1. For the GPL license (GPL), you can redistribute and/or + * modify this file under the terms of the GNU General + * Public License, Version 3, as published by the Free Software + * Foundation. You should have received a copy of the GNU + * General Public License, Version 3 along with this program; + * if not, write to the Free Software Foundation, Inc., 51 + * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * 2. For the Magnolia Network Agreement (MNA), this file + * and the accompanying materials are made available under the + * terms of the MNA which accompanies this distribution, and + * is available at http://www.magnolia-cms.com/mna.html + * + * Any modifications to this file must keep this entire header + * intact. + * + */ +package info.magnolia.module.rssaggregator.util; + +import info.magnolia.cms.core.Content; + +import javax.jcr.RepositoryException; + +/** + * Capable of mapping a {@link Content content} node {@link #map(Content) to} an object of type T. + * + * @author Rob van der Linden Vooren + */ +public interface ContentMapper { + + /** + * Map a content node that defines a type T to an object of that type. + * + * @param content the content node to map + * @return the result object of type T + * @throws RepositoryException when a problem occurred accessing the underlying repository + */ + T map(Content content) throws RepositoryException; + +} Index: src/main/java/info/magnolia/module/rssaggregator/generator/FeedGenerator.java =================================================================== --- src/main/java/info/magnolia/module/rssaggregator/generator/FeedGenerator.java Sun Feb 22 21:14:43 CET 2009 +++ src/main/java/info/magnolia/module/rssaggregator/generator/FeedGenerator.java Sun Feb 22 21:14:43 CET 2009 @@ -0,0 +1,52 @@ +/** + * This file Copyright (c) 2008-2009 Magnolia International + * Ltd. (http://www.magnolia-cms.com). All rights reserved. + * + * + * This file is dual-licensed under both the Magnolia + * Network Agreement and the GNU General Public License. + * You may elect to use one or the other of these licenses. + * + * This file is distributed in the hope that it will be + * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the + * implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT. + * Redistribution, except as permitted by whichever of the GPL + * or MNA you select, is prohibited. + * + * 1. For the GPL license (GPL), you can redistribute and/or + * modify this file under the terms of the GNU General + * Public License, Version 3, as published by the Free Software + * Foundation. You should have received a copy of the GNU + * General Public License, Version 3 along with this program; + * if not, write to the Free Software Foundation, Inc., 51 + * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * 2. For the Magnolia Network Agreement (MNA), this file + * and the accompanying materials are made available under the + * terms of the MNA which accompanies this distribution, and + * is available at http://www.magnolia-cms.com/mna.html + * + * Any modifications to this file must keep this entire header + * intact. + * + */ +package info.magnolia.module.rssaggregator.generator; + +/** + * Strategy interface for generating a Feed. + * + * @author Rob van der Linden Vooren + * @see Feed + */ +public interface FeedGenerator { + + /** + * Generate a Feed. + * + * @return the generated feed + * @throws FeedGenerationException if bad things happend while generating the feed + */ + Feed generate() throws FeedGenerationException; + +} Index: pom.xml =================================================================== --- pom.xml (revision 22801) +++ pom.xml Sun Feb 22 18:14:06 CET 2009 @@ -1,115 +1,120 @@ - 4.0.0 - - magnolia-parent-pom-community-module - info.magnolia - 6 - - magnolia-module-rssaggregator - 1.0-SNAPSHOT - jar - magnolia-module-rssaggregator - - - 4.0-SNAPSHOT - http://documentation.magnolia-cms.com/modules/rssaggregator.html - - - Jira - http://jira.magnolia-cms.com/browse/MGNLRSSAGG - - + 4.0.0 + + magnolia-parent-pom-community-module + info.magnolia + 6 + + magnolia-module-rssaggregator + 1.0-SNAPSHOT + jar + magnolia-module-rssaggregator + + + 4.0-SNAPSHOT + http://documentation.magnolia-cms.com/modules/rssaggregator.html + + + Jira + http://jira.magnolia-cms.com/browse/MGNLRSSAGG + + - scm:svn:http://svn.magnolia-cms.com/svn/community/modules/magnolia-module-rssaggregator/trunk - scm:svn:http://svn.magnolia-cms.com/svn/community/modules/magnolia-module-rssaggregator/trunk + scm:svn:http://svn.magnolia-cms.com/svn/community/modules/magnolia-module-rssaggregator/trunk + + + scm:svn:http://svn.magnolia-cms.com/svn/community/modules/magnolia-module-rssaggregator/trunk + - http://svn.magnolia-cms.com/view/community/modules/magnolia-module-rssaggregator/trunk - - - - info.magnolia - magnolia-core - ${magnoliaVersion} - provided - - - info.magnolia - magnolia-gui - ${magnoliaVersion} - provided - - - info.magnolia - magnolia-module-admininterface - ${magnoliaVersion} - provided - - - info.magnolia - magnolia-module-data - 1.3-SNAPSHOT - provided - - - info.magnolia - magnolia-module-scheduler - 1.2 - provided - - - rome - rome + http://svn.magnolia-cms.com/view/community/modules/magnolia-module-rssaggregator/trunk + + + + info.magnolia + magnolia-core + ${magnoliaVersion} + provided + + + info.magnolia + magnolia-gui + ${magnoliaVersion} + provided + + + info.magnolia + magnolia-module-admininterface + ${magnoliaVersion} + provided + + + info.magnolia + magnolia-module-data + 1.3-SNAPSHOT + provided + + + info.magnolia + magnolia-module-scheduler + 1.2 + provided + + + rome + rome - 1.0RC2 + 0.9 - - - rome - rome-fetcher + + + rome + rome-fetcher - 1.0RC2 + 0.9 - - - commons-logging - commons-logging - - - - - - junit - junit - 4.4 + + + commons-logging + commons-logging + + + + + + junit + junit + 4.4 + test - - - org.easymock - easymockclassextension - 2.4 + + + org.easymock + easymockclassextension + 2.4 + test - - - - - - org.apache.maven.plugins - maven-assembly-plugin - - - org.apache.maven.plugins - maven-compiler-plugin - - 1.5 - 1.5 - - - - - - - - sun-repo-2 - http://download.java.net/maven/2/ - - true - - - false - - - + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.5 + 1.5 + + + + + + + + sun-repo-2 + http://download.java.net/maven/2/ + + true + + + false + + + Index: src/main/java/info/magnolia/module/rssaggregator/importhandler/AggregateFeedContentMapper.java =================================================================== --- src/main/java/info/magnolia/module/rssaggregator/importhandler/AggregateFeedContentMapper.java (revision 22801) +++ src/main/java/info/magnolia/module/rssaggregator/importhandler/AggregateFeedContentMapper.java Sat Feb 21 20:58:09 CET 2009 @@ -35,6 +35,7 @@ import info.magnolia.cms.core.Content; import info.magnolia.module.rssaggregator.util.Assert; +import info.magnolia.module.rssaggregator.util.ContentMapper; import javax.jcr.RepositoryException; import java.util.Collection; @@ -56,7 +57,9 @@ feedChannelMapper = new FeedChannelContentMapper(); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ @SuppressWarnings("unchecked") public AggregateFeed map(Content aggregateNode) throws RepositoryException { AggregateFeed aggregateFeed = new AggregateFeed(aggregateNode.getName()); @@ -74,7 +77,9 @@ // Getters & setters - /** for testing */ + /** + * for testing + */ protected void setFeedChannelContentMapper(FeedChannelContentMapper feedChannelMapper) { Assert.notNull(feedChannelMapper, "'feedChannelMapper' must not be null"); this.feedChannelMapper = feedChannelMapper;