Index: xwiki-platform-core/xwiki-core/src/test/java/com/xpn/xwiki/plugin/feed/SyndEntryDocumentSourceTest.java
===================================================================
--- xwiki-platform-core/xwiki-core/src/test/java/com/xpn/xwiki/plugin/feed/SyndEntryDocumentSourceTest.java (revision 0)
+++ xwiki-platform-core/xwiki-core/src/test/java/com/xpn/xwiki/plugin/feed/SyndEntryDocumentSourceTest.java (revision 0)
@@ -0,0 +1,353 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package com.xpn.xwiki.plugin.feed;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.jmock.Mock;
+import org.jmock.core.Invocation;
+import org.jmock.core.stub.CustomStub;
+import org.xwiki.component.manager.ComponentManager;
+
+import com.sun.syndication.feed.synd.SyndEntry;
+import com.sun.syndication.feed.synd.SyndEntryImpl;
+import com.xpn.xwiki.XWiki;
+import com.xpn.xwiki.XWikiConfig;
+import com.xpn.xwiki.XWikiContext;
+import com.xpn.xwiki.XWikiException;
+import com.xpn.xwiki.api.Document;
+import com.xpn.xwiki.doc.XWikiDocument;
+import com.xpn.xwiki.objects.classes.BaseClass;
+import com.xpn.xwiki.store.XWikiHibernateStore;
+import com.xpn.xwiki.store.XWikiHibernateVersioningStore;
+import com.xpn.xwiki.store.XWikiStoreInterface;
+import com.xpn.xwiki.store.XWikiVersioningStoreInterface;
+import com.xpn.xwiki.test.AbstractXWikiComponentTestCase;
+import com.xpn.xwiki.user.api.XWikiRightService;
+import com.xpn.xwiki.user.impl.xwiki.XWikiRightServiceImpl;
+import com.xpn.xwiki.web.XWikiServletURLFactory;
+
+/**
+ * Unit tests for {@link SyndEntryDocumentSource}.
+ */
+public class SyndEntryDocumentSourceTest extends AbstractXWikiComponentTestCase
+{
+ public static final String INCONSISTENCY = "Inconsistency!";
+
+ public static final String POLYMORPHISM_INCONSISTENCY = "Polymorphism inconsistency!";
+
+ public static final String ACCESS_RIGHTS_VIOLATED = "Access rights are violated!";
+
+ public static final String PARAMETERS_IGNORED = "Parameters are ignored!";
+
+ public static final String SVG_MIME_TYPE = "image/svg+xml";
+
+ public static final String PNG_MIME_TYPE = "image/png";
+
+ public static final String ARTICLE_CLASS_NAME = "XWiki.ArticleClass";
+
+ protected SyndEntryDocumentSource source;
+
+ protected XWikiContext context;
+
+ protected XWikiDocument doc;
+
+ protected void setUp() throws Exception
+ {
+ super.setUp();
+
+ context = newXWikiContext();
+ context.setUser("Condor");
+
+ doc = new XWikiDocument("MilkyWay", "Fidis");
+ doc.setCreator("Condor");
+ doc.setAuthor("Albatross");
+ doc.setTitle("Fidis from MilkyWay");
+ doc.setContent("blah blah blah..");
+
+ initArticleClass(context);
+
+ doc.createNewObject(ARTICLE_CLASS_NAME, context);
+ doc.setStringValue(ARTICLE_CLASS_NAME, "title", "Old story");
+ doc.setStringValue(ARTICLE_CLASS_NAME, "content", "Once upon a time there was..");
+ List categories = new ArrayList();
+ categories.add("News");
+ categories.add("Information");
+ doc.setStringListValue(ARTICLE_CLASS_NAME, "category", categories);
+
+ context.getWiki().saveDocument(doc, context);
+ context.setDoc(doc);
+
+ source = new SyndEntryDocumentSource();
+ }
+
+ private XWikiContext newXWikiContext() throws Exception
+ {
+ final Map docs = new HashMap();
+ final XWikiContext context = new XWikiContext();
+ final XWiki xwiki = new XWiki(new XWikiConfig(), context);
+ context.setWiki(xwiki);
+ context.setURLFactory(new XWikiServletURLFactory(new URL("http://www.xwiki.org/"),
+ "xwiki/",
+ "bin/"));
+ // We need to initialize the Component Manager so that components can be looked up
+ context.put(ComponentManager.class.getName(), getComponentManager());
+
+ final Mock mockXWikiStore =
+ mock(XWikiHibernateStore.class, new Class[] {XWiki.class, XWikiContext.class},
+ new Object[] {xwiki, context});
+ mockXWikiStore.stubs().method("loadXWikiDoc").will(
+ new CustomStub("Implements XWikiStoreInterface.loadXWikiDoc")
+ {
+ public Object invoke(Invocation invocation) throws Throwable
+ {
+ XWikiDocument shallowDoc = (XWikiDocument) invocation.parameterValues.get(0);
+ if (docs.containsKey(shallowDoc.getName())) {
+ return (XWikiDocument) docs.get(shallowDoc.getName());
+ } else {
+ return shallowDoc;
+ }
+ }
+ });
+ mockXWikiStore.stubs().method("saveXWikiDoc").will(
+ new CustomStub("Implements XWikiStoreInterface.saveXWikiDoc")
+ {
+ public Object invoke(Invocation invocation) throws Throwable
+ {
+ XWikiDocument document = (XWikiDocument) invocation.parameterValues.get(0);
+ document.setNew(false);
+ document.setStore((XWikiStoreInterface) mockXWikiStore.proxy());
+ docs.put(document.getName(), document);
+ return null;
+ }
+ });
+ mockXWikiStore.stubs().method("getTranslationList").will(
+ returnValue(Collections.EMPTY_LIST));
+
+ final Mock mockXWikiVersioningStore =
+ mock(XWikiHibernateVersioningStore.class, new Class[] {XWiki.class,
+ XWikiContext.class}, new Object[] {xwiki, this.context});
+ mockXWikiVersioningStore.stubs().method("getXWikiDocumentArchive")
+ .will(returnValue(null));
+ mockXWikiVersioningStore.stubs().method("resetRCSArchive").will(returnValue(null));
+
+ xwiki.setStore((XWikiStoreInterface) mockXWikiStore.proxy());
+ xwiki
+ .setVersioningStore((XWikiVersioningStoreInterface) mockXWikiVersioningStore.proxy());
+
+ final Mock mockXWikiRightsService =
+ mock(XWikiRightServiceImpl.class, new Class[] {}, new Object[] {});
+ mockXWikiRightsService.stubs().method("hasAccessLevel").will(
+ new CustomStub("Implements XWikiRightService.hasAccessLevel")
+ {
+ public Object invoke(Invocation invocation) throws Throwable
+ {
+ // String right = (String) invocation.parameterValues.get(0);
+ String user = (String) invocation.parameterValues.get(1);
+ // String doc = (String) invocation.parameterValues.get(2);
+ // we give access to all the users with an even name length
+ return new Boolean(user.length() % 2 == 0);
+ }
+ });
+ xwiki.setRightService((XWikiRightService) mockXWikiRightsService.proxy());
+
+ return context;
+ }
+
+ protected BaseClass initArticleClass(XWikiContext context) throws XWikiException
+ {
+ XWikiDocument doc;
+ boolean needsUpdate = false;
+
+ try {
+ doc = context.getWiki().getDocument(ARTICLE_CLASS_NAME, context);
+ } catch (Exception e) {
+ doc = new XWikiDocument();
+ doc.setFullName(ARTICLE_CLASS_NAME);
+ needsUpdate = true;
+ }
+
+ BaseClass bclass = doc.getxWikiClass();
+ bclass.setName(ARTICLE_CLASS_NAME);
+
+ needsUpdate |= bclass.addTextField("title", "Title", 64);
+ needsUpdate |= bclass.addTextAreaField("content", "Content", 45, 4);
+ needsUpdate |= bclass.addTextField("category", "Category", 64);
+
+ String content = doc.getContent();
+ if ((content == null) || (content.equals(""))) {
+ needsUpdate = true;
+ doc.setContent("1 XWiki.ArticleClass");
+ }
+
+ if (needsUpdate) {
+ context.getWiki().saveDocument(doc, context);
+ }
+ return bclass;
+ }
+
+ protected SyndEntryImpl source(Object obj)
+ {
+ return source(obj, Collections.EMPTY_MAP);
+ }
+
+ protected SyndEntryImpl source(Object obj, Map params)
+ {
+ SyndEntryImpl entry = new SyndEntryImpl();
+ try {
+ source.source(entry, obj, params, context);
+ } catch (Exception e) {
+ }
+ return entry;
+ }
+
+ /**
+ * Computes the sum of lengths of all the text nodes from the given XML fragment.
+ *
+ * @param xmlFragment the XML fragment to be parsed
+ * @return the number of characters in all the text nodes within the given XML fragment
+ */
+ protected int getXMLContentLength(String xmlFragment)
+ {
+ return SyndEntryDocumentSource.innerTextLength(SyndEntryDocumentSource.tidy(xmlFragment,
+ SyndEntryDocumentSource.TIDY_HTML_CONFIG));
+ }
+
+ /**
+ * Tests if two successive calls of the source method with the same argument have the same
+ * result.
+ */
+ public void testSourceConsistency()
+ {
+ assertEquals(INCONSISTENCY, source(doc), source(doc));
+ }
+
+ /**
+ * Tests if different calls of the source method have the same result when the argument passed
+ * points to the same document, irrespective of its type: {@link XWikiDocument},
+ * {@link Document}, and {@link String}.
+ */
+ public void testSourcePolymorphism()
+ {
+ SyndEntryImpl fromXDoc = source(doc);
+ SyndEntryImpl fromDoc = source(doc.newDocument(context));
+ SyndEntryImpl fromFullName = source(doc.getFullName());
+ assertEquals(POLYMORPHISM_INCONSISTENCY, fromXDoc, fromDoc);
+ assertEquals(POLYMORPHISM_INCONSISTENCY, fromXDoc, fromFullName);
+ assertEquals(POLYMORPHISM_INCONSISTENCY, fromDoc, fromFullName);
+ }
+
+ /**
+ * Tests if the source method obeys the access rights.
+ */
+ public void testSourceAccessRights()
+ {
+ // odd user name length implies no access rights
+ context.setUser("XWiki.Albatross");
+ try {
+ source.source(new SyndEntryImpl(), doc, Collections.EMPTY_MAP, context);
+ assertTrue(ACCESS_RIGHTS_VIOLATED, false);
+ } catch (XWikiException e) {
+ // we should get an exception
+ }
+ // even user name length implies all access rights
+ context.setUser("Condor");
+ try {
+ source.source(new SyndEntryImpl(), doc, Collections.EMPTY_MAP, context);
+ // we shouldn't get an exception
+ } catch (XWikiException e) {
+ assertTrue(ACCESS_RIGHTS_VIOLATED, false);
+ }
+ }
+
+ /**
+ * Tests if {@link SyndEntryDocumentSource.CONTENT_TYPE} parameter is used correctly.
+ */
+ public void testSourceContentType()
+ {
+ Map instanceParams = new HashMap();
+ instanceParams.put(SyndEntryDocumentSource.CONTENT_TYPE, SVG_MIME_TYPE);
+ source.setParams(instanceParams);
+ assertEquals(PARAMETERS_IGNORED, SVG_MIME_TYPE, source(doc).getDescription().getType());
+
+ Map methodParams = new HashMap();
+ methodParams.put(SyndEntryDocumentSource.CONTENT_TYPE, PNG_MIME_TYPE);
+ SyndEntry entry = source(doc, methodParams);
+ assertEquals(PARAMETERS_IGNORED, PNG_MIME_TYPE, entry.getDescription().getType());
+ }
+
+ /**
+ * Tests if {@link SyndEntryDocumentSource#CONTENT_LENGTH} parameter is used correctly when the
+ * {@link SyndEntryDocumentSource#CONTENT_TYPE} is text/plain.
+ */
+ public void testArticleSourcePlainContentLength()
+ {
+ int maxLength = 15;
+ Map params = new HashMap();
+ params.put(SyndEntryDocumentSource.CONTENT_TYPE, "text/plain");
+ params.put(SyndEntryDocumentSource.CONTENT_LENGTH, new Integer(maxLength));
+ params.put(SyndEntryDocumentSource.FIELD_DESCRIPTION, ARTICLE_CLASS_NAME + "_content");
+ source.setParams(params);
+ doc.setStringValue(ARTICLE_CLASS_NAME, "content", "Somewhere in la Mancha, in a place..");
+ assertTrue(doc.display("content", context).length() > maxLength);
+ int descriptionLength = source(doc).getDescription().getValue().length();
+ assertTrue(PARAMETERS_IGNORED, descriptionLength <= maxLength);
+ }
+
+ /**
+ * Tests if {@link SyndEntryDocumentSource#CONTENT_LENGTH} parameter is used correctly when the
+ * {@link SyndEntryDocumentSource#CONTENT_TYPE} is text/html.
+ */
+ public void testArticleSourceHTMLContentLength()
+ {
+ int maxLength = 16;
+ Map params = new HashMap();
+ params.put(SyndEntryDocumentSource.CONTENT_TYPE, "text/html");
+ params.put(SyndEntryDocumentSource.CONTENT_LENGTH, new Integer(maxLength));
+ params.put(SyndEntryDocumentSource.FIELD_DESCRIPTION, ARTICLE_CLASS_NAME + "_content");
+ doc
+ .setStringValue(ARTICLE_CLASS_NAME, "content",
+ "Somewhere \n\tin la Mancha, in a place..");
+ assertTrue(getXMLContentLength(doc.display("content", context)) > maxLength);
+ String description = source(doc, params).getDescription().getValue();
+ int descriptionLength = getXMLContentLength(description);
+ assertTrue(PARAMETERS_IGNORED, descriptionLength <= maxLength);
+ }
+
+ public void testArticleSourceXMLContentLength()
+ {
+ int maxLength = 17;
+ Map params = new HashMap();
+ params.put(SyndEntryDocumentSource.CONTENT_TYPE, "text/xml");
+ params.put(SyndEntryDocumentSource.CONTENT_LENGTH, new Integer(maxLength));
+ params.put(SyndEntryDocumentSource.FIELD_DESCRIPTION, ARTICLE_CLASS_NAME + "_content");
+ doc.setStringValue(ARTICLE_CLASS_NAME, "content",
+ "Somewhere \n\tin la Mancha, in a place..");
+ assertTrue(getXMLContentLength(doc.display("content", context)) > maxLength);
+ String description = source(doc, params).getDescription().getValue();
+ int descriptionLength = getXMLContentLength(description);
+ assertTrue(PARAMETERS_IGNORED, descriptionLength <= maxLength);
+ }
+}
Index: xwiki-platform-core/xwiki-core/src/main/java/com/xpn/xwiki/plugin/feed/SyndEntryDocumentSource.java
===================================================================
--- xwiki-platform-core/xwiki-core/src/main/java/com/xpn/xwiki/plugin/feed/SyndEntryDocumentSource.java (revision 0)
+++ xwiki-platform-core/xwiki-core/src/main/java/com/xpn/xwiki/plugin/feed/SyndEntryDocumentSource.java (revision 0)
@@ -0,0 +1,775 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package com.xpn.xwiki.plugin.feed;
+
+import java.io.ByteArrayInputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.sax.SAXResult;
+
+import org.w3c.dom.Node;
+import org.w3c.tidy.Tidy;
+
+import com.sun.syndication.feed.synd.SyndContent;
+import com.sun.syndication.feed.synd.SyndContentImpl;
+import com.sun.syndication.feed.synd.SyndEntry;
+import com.xpn.xwiki.XWiki;
+import com.xpn.xwiki.XWikiContext;
+import com.xpn.xwiki.XWikiException;
+import com.xpn.xwiki.api.Document;
+import com.xpn.xwiki.doc.XWikiDocument;
+import com.xpn.xwiki.util.ExtractHandler;
+
+/**
+ * Concrete strategy for computing the field values of a feed entry from any {@link XWikiDocument}
+ * instance.
+ */
+public class SyndEntryDocumentSource implements SyndEntrySource
+{
+ /**
+ * Utility class for selecting a property from a XWiki object.
+ */
+ public static class PropertySelector
+ {
+ /**
+ * The name of a XWiki class.
+ */
+ private String className;
+
+ /**
+ * The index of an object within the document that this selector is applied to.
+ */
+ private int objectIndex;
+
+ /**
+ * The name of a property available for {@link #className}.
+ */
+ private String propertyName;
+
+ /**
+ * Creates a new instance from a string representation.
+ *
+ * @param strRep a string like "ClassName_ObjectIndex_PropertyName", where class name and
+ * object index are optional
+ */
+ public PropertySelector(String strRep)
+ {
+ int indexStartPos = strRep.indexOf('_');
+ if (indexStartPos < 0) {
+ // class name and object index are not specified
+ className = null;
+ objectIndex = 0;
+ propertyName = strRep;
+ } else {
+ int propStartPos = strRep.indexOf("_", indexStartPos + 1);
+ if (propStartPos < 0) {
+ // object index is not specified
+ className = strRep.substring(0, indexStartPos);
+ objectIndex = 0;
+ propertyName = strRep.substring(indexStartPos + 1);
+ } else {
+ // all three have been specified
+ className = strRep.substring(0, indexStartPos);
+ objectIndex =
+ Integer.parseInt(strRep.substring(indexStartPos + 1, propStartPos));
+ propertyName = strRep.substring(propStartPos + 1);
+ }
+ }
+ }
+
+ /**
+ * @return the name of a XWiki class
+ */
+ public String getClassName()
+ {
+ return className;
+ }
+
+ /**
+ * @return the index of an object within the document that this selector is applied to
+ */
+ public int getObjectIndex()
+ {
+ return objectIndex;
+ }
+
+ /**
+ * @return the name of a property available for {@link #className}
+ */
+ public String getPropertyName()
+ {
+ return propertyName;
+ }
+ }
+
+ public static final String CONTENT_TYPE = "ContentType";
+
+ public static final String CONTENT_LENGTH = "ContentLength";
+
+ public static final Properties TIDY_FEED_CONFIG;
+
+ public static final Properties TIDY_XML_CONFIG;
+
+ public static final Properties TIDY_HTML_CONFIG;
+
+ public static final String FIELD_URI = "uri";
+
+ public static final String FIELD_LINK = "link";
+
+ public static final String FIELD_TITLE = "title";
+
+ public static final String FIELD_DESCRIPTION = "description";
+
+ public static final String FIELD_CATEGORIES = "categories";
+
+ public static final String FIELD_PUBLISHED_DATE = "publishedDate";
+
+ public static final String FIELD_UPDATED_DATE = "updatedDate";
+
+ public static final String FIELD_AUTHOR = "author";
+
+ public static final String FIELD_CONTRIBUTORS = "contributors";
+
+ public static final Map DEFAULT_PARAMS;
+
+ static {
+ // general configuration
+ TIDY_FEED_CONFIG = new Properties();
+ TIDY_FEED_CONFIG.setProperty("force-output", "yes");
+ TIDY_FEED_CONFIG.setProperty("indent-attributes", "no");
+ TIDY_FEED_CONFIG.setProperty("indent", "no");
+ TIDY_FEED_CONFIG.setProperty("quiet", "yes");
+ TIDY_FEED_CONFIG.setProperty("trim-empty-elements", "yes");
+
+ // XML specific configuration
+ TIDY_XML_CONFIG = new Properties(TIDY_FEED_CONFIG);
+ TIDY_XML_CONFIG.setProperty("input-xml", "yes");
+ TIDY_XML_CONFIG.setProperty("output-xml", "yes");
+ TIDY_XML_CONFIG.setProperty("add-xml-pi", "no");
+
+ // HTML specific configuration
+ TIDY_HTML_CONFIG = new Properties(TIDY_FEED_CONFIG);
+ TIDY_HTML_CONFIG.setProperty("output-xhtml", "yes");
+ TIDY_HTML_CONFIG.setProperty("print-body-only", "yes");
+ TIDY_HTML_CONFIG.setProperty("drop-empty-paras", "yes");
+ TIDY_HTML_CONFIG.setProperty("enclose-text", "yes");
+ TIDY_HTML_CONFIG.setProperty("logical-emphasis", "yes");
+
+ // default parameters for all instances of this class
+ DEFAULT_PARAMS = new HashMap();
+ DEFAULT_PARAMS.put(CONTENT_TYPE, "text/html");
+ DEFAULT_PARAMS.put(CONTENT_LENGTH, new Integer(250));
+ }
+
+ /**
+ * Strategy instance parameters. Each concrete strategy can define its own (paramName,
+ * paramValue) pairs. These parameters are overwritten by those used when calling
+ * {@link SyndEntrySource#source(SyndEntry, Object, Map, XWikiContext)} method
+ */
+ private Map params;
+
+ public SyndEntryDocumentSource()
+ {
+ this(Collections.EMPTY_MAP);
+ }
+
+ /**
+ * Creates a new instance. The given parameters overwrite {@link #DEFAULT_PARAMS}.
+ *
+ * @param params parameters only for this instance
+ */
+ public SyndEntryDocumentSource(Map params)
+ {
+ setParams(params);
+ }
+
+ /**
+ * @return instance parameters
+ */
+ public Map getParams()
+ {
+ return params;
+ }
+
+ /**
+ * Sets instance parameters. Instance parameters overwrite {@link #DEFAULT_PARAMS}
+ *
+ * @param params instance parameters
+ */
+ public void setParams(Map params)
+ {
+ this.params = joinParams(params, getDefaultParams());
+ }
+
+ /**
+ * Strategy class parameters
+ */
+ protected Map getDefaultParams()
+ {
+ return DEFAULT_PARAMS;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see SyndEntrySource#source(SyndEntry, Object, Map, XWikiContext)
+ */
+ public void source(SyndEntry entry, Object obj, Map params, XWikiContext context)
+ throws XWikiException
+ {
+ // cast source
+ Document doc = castDocument(obj, context);
+
+ // test access rights
+ if (!doc.hasAccessLevel("view")) {
+ throw new XWikiException();
+ }
+
+ // prepare parameters (overwrite instance parameters)
+ Map trueParams = joinParams(params, getParams());
+
+ sourceDocument(entry, doc, trueParams, context);
+ }
+
+ /**
+ * Overwrites the current values of the given feed entry with new ones computed from the
+ * specified source document.
+ *
+ * @param entry the feed entry whose fields are going to be overwritten
+ * @param doc the source for the new values to be set on the fields of the feed entry
+ * @param params parameters to adjust the computation. Each concrete strategy may define its own
+ * (key, value) pairs
+ * @param context the XWiki context
+ * @throws XWikiException
+ */
+ public void sourceDocument(SyndEntry entry, Document doc, Map params, XWikiContext context)
+ throws XWikiException
+ {
+ entry.setUri(getURI(doc, params, context));
+ entry.setLink(getLink(doc, params, context));
+ entry.setTitle(getTitle(doc, params, context));
+ entry.setDescription(getDescription(doc, params, context));
+ entry.setCategories(getCategories(doc, params, context));
+ entry.setPublishedDate(getPublishedDate(doc, params, context));
+ entry.setUpdatedDate(getUpdateDate(doc, params, context));
+ entry.setAuthor(getAuthor(doc, params, context));
+ entry.setContributors(getContributors(doc, params, context));
+ }
+
+ protected String getDefaultURI(Document doc, Map params, XWikiContext context)
+ throws XWikiException
+ {
+ return doc.getExternalURL("view", "language=" + doc.getRealLanguage());
+ }
+
+ protected String getURI(Document doc, Map params, XWikiContext context) throws XWikiException
+ {
+ String mapping = (String) params.get(FIELD_URI);
+ if (mapping == null) {
+ return getDefaultURI(doc, params, context);
+ } else if (isVelocityCode(mapping)) {
+ return parseString(mapping, doc, context);
+ } else {
+ return getStringValue(mapping, doc, context);
+ }
+ }
+
+ protected String getDefaultLink(Document doc, Map params, XWikiContext context)
+ throws XWikiException
+ {
+ return getDefaultURI(doc, params, context);
+ }
+
+ protected String getLink(Document doc, Map params, XWikiContext context)
+ throws XWikiException
+ {
+ String mapping = (String) params.get(FIELD_LINK);
+ if (mapping == null) {
+ return getDefaultLink(doc, params, context);
+ } else if (isVelocityCode(mapping)) {
+ return parseString(mapping, doc, context);
+ } else {
+ return getStringValue(mapping, doc, context);
+ }
+ }
+
+ protected String getDefaultTitle(Document doc, Map params, XWikiContext context)
+ {
+ return doc.getDisplayTitle();
+ }
+
+ protected String getTitle(Document doc, Map params, XWikiContext context)
+ throws XWikiException
+ {
+ String mapping = (String) params.get(FIELD_TITLE);
+ if (mapping == null) {
+ return getDefaultTitle(doc, params, context);
+ } else if (isVelocityCode(mapping)) {
+ return parseString(mapping, doc, context);
+ } else {
+ return getStringValue(mapping, doc, context);
+ }
+ }
+
+ protected String getDefaultDescription(Document doc, Map params, XWikiContext context)
+ {
+ XWiki xwiki = context.getWiki();
+ String author = xwiki.getUserName(doc.getAuthor(), null, false, context);
+ // the description format should be taken from a resource bundle, and thus localized
+ String descFormat = "Version %1$s edited by %2$s on %3$s";
+ return String.format(descFormat, new Object[] {doc.getVersion(), author, doc.getDate()});
+ }
+
+ protected SyndContent getDescription(Document doc, Map params, XWikiContext context)
+ throws XWikiException
+ {
+ String description;
+ String mapping = (String) params.get(FIELD_DESCRIPTION);
+ if (mapping == null) {
+ description = getDefaultDescription(doc, params, context);
+ } else if (isVelocityCode(mapping)) {
+ description = parseString(mapping, doc, context);
+ } else {
+ description = getStringValue(mapping, doc, context);
+ description = context.getDoc().getRenderedContent(description, context);
+ }
+ String contentType = (String) params.get(CONTENT_TYPE);
+ int contentLength = ((Number) params.get(CONTENT_LENGTH)).intValue();
+ if ("text/plain".equals(contentType)) {
+ description = getPlainPreview(description, contentLength);
+ } else if ("text/html".equals(contentType)) {
+ description = getHTMLPreview(description, contentLength);
+ } else if ("text/xml".equals(contentType)) {
+ description = getXMLPreview(description, contentLength);
+ }
+ return getSyndContent(contentType, description);
+ }
+
+ protected List getDefaultCategories(Document doc, Map params, XWikiContext context)
+ {
+ return Collections.EMPTY_LIST;
+ }
+
+ protected List getCategories(Document doc, Map params, XWikiContext context)
+ throws XWikiException
+ {
+ String mapping = (String) params.get(FIELD_CATEGORIES);
+ if (mapping == null) {
+ return getDefaultCategories(doc, params, context);
+ } else if (isVelocityCode(mapping)) {
+ return parseList(mapping, doc, context);
+ } else {
+ return getListValue(mapping, doc, context);
+ }
+ }
+
+ protected Date getDefaultPublishedDate(Document doc, Map params, XWikiContext context)
+ {
+ return doc.getCreationDate();
+ }
+
+ protected Date getPublishedDate(Document doc, Map params, XWikiContext context)
+ throws XWikiException
+ {
+ String mapping = (String) params.get(FIELD_PUBLISHED_DATE);
+ if (mapping == null) {
+ return getDefaultPublishedDate(doc, params, context);
+ } else if (isVelocityCode(mapping)) {
+ return parseDate(mapping, doc, context);
+ } else {
+ return getDateValue(mapping, doc, context);
+ }
+ }
+
+ protected Date getDefaultUpdateDate(Document doc, Map params, XWikiContext context)
+ {
+ return doc.getDate();
+ }
+
+ protected Date getUpdateDate(Document doc, Map params, XWikiContext context)
+ throws XWikiException
+ {
+ String mapping = (String) params.get(FIELD_UPDATED_DATE);
+ if (mapping == null) {
+ return getDefaultUpdateDate(doc, params, context);
+ } else if (isVelocityCode(mapping)) {
+ return parseDate(mapping, doc, context);
+ } else {
+ return getDateValue(mapping, doc, context);
+ }
+ }
+
+ protected String getDefaultAuthor(Document doc, Map params, XWikiContext context)
+ {
+ return context.getWiki().getUserName(doc.getCreator(), null, false, context);
+ }
+
+ protected String getAuthor(Document doc, Map params, XWikiContext context)
+ throws XWikiException
+ {
+ String mapping = (String) params.get(FIELD_AUTHOR);
+ if (mapping == null) {
+ return getDefaultAuthor(doc, params, context);
+ } else if (isVelocityCode(mapping)) {
+ return parseString(mapping, doc, context);
+ } else {
+ return getStringValue(mapping, doc, context);
+ }
+ }
+
+ protected List getDefaultContributors(Document doc, Map params, XWikiContext context)
+ {
+ XWiki xwiki = context.getWiki();
+ List contributors = new ArrayList();
+ contributors.add(xwiki.getUserName(doc.getAuthor(), null, false, context));
+ return contributors;
+ }
+
+ protected List getContributors(Document doc, Map params, XWikiContext context)
+ throws XWikiException
+ {
+ String mapping = (String) params.get(FIELD_CONTRIBUTORS);
+ if (mapping == null) {
+ return getDefaultContributors(doc, params, context);
+ } else if (isVelocityCode(mapping)) {
+ return parseList(mapping, doc, context);
+ } else {
+ return getListValue(mapping, doc, context);
+ }
+ }
+
+ /**
+ * Distinguishes between mapping to a property and mapping to a velocity code.
+ *
+ * @param mapping
+ * @return true if the given string is a mapping to a velocity code
+ */
+ protected boolean isVelocityCode(String mapping)
+ {
+ return mapping.charAt(0) == '{' && mapping.charAt(mapping.length() - 1) == '}';
+ }
+
+ protected String parseString(String mapping, Document doc, XWikiContext context)
+ throws XWikiException
+ {
+ if (isVelocityCode(mapping)) {
+ return doc.getRenderedContent(mapping.substring(1, mapping.length() - 1));
+ } else {
+ return mapping;
+ }
+ }
+
+ /**
+ * Converts the given velocity string to a {@link Date} instance. The velocity code must be
+ * evaluated to a long representing a time stamp.
+ */
+ protected Date parseDate(String mapping, Document doc, XWikiContext context)
+ throws NumberFormatException, XWikiException
+ {
+ if (isVelocityCode(mapping)) {
+ return new Date(Long.parseLong(parseString(mapping, doc, context).trim()));
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Converts the given velocity code to a {@link List} instance. The velocity code must be
+ * evaluated to a string with the following format: "[item1,item2,...,itemN]".
+ */
+ protected List parseList(String mapping, Document doc, XWikiContext context)
+ throws XWikiException
+ {
+ if (!isVelocityCode(mapping)) {
+ return null;
+ }
+ String strRep = parseString(mapping, doc, context).trim();
+ if (strRep.charAt(0) != '[' || strRep.charAt(strRep.length() - 1) != ']') {
+ return null;
+ }
+ String[] array = strRep.substring(1, strRep.length() - 1).split(",");
+ if (array.length > 0) {
+ List list = new ArrayList();
+ for (int i = 0; i < array.length; i++) {
+ list.add(array[i]);
+ }
+ return list;
+ } else {
+ return Collections.EMPTY_LIST;
+ }
+ }
+
+ /**
+ * Retrieves the value of a string property.
+ */
+ protected String getStringValue(String mapping, Document doc, XWikiContext context)
+ throws XWikiException
+ {
+ PropertySelector ps = new PropertySelector(mapping);
+ if (ps.getClassName() == null) {
+ return doc.display(ps.getPropertyName());
+ } else {
+ XWikiDocument xdoc = context.getWiki().getDocument(doc.getFullName(), context);
+ return xdoc.getObject(ps.getClassName(), ps.getObjectIndex()).getStringValue(
+ ps.getPropertyName());
+ }
+ }
+
+ /**
+ * Retrieves the value of a date property.
+ */
+ protected Date getDateValue(String mapping, Document doc, XWikiContext context)
+ throws XWikiException
+ {
+ XWikiDocument xdoc = context.getWiki().getDocument(doc.getFullName(), context);
+ PropertySelector ps = new PropertySelector(mapping);
+ if (ps.getClassName() == null) {
+ return xdoc.getFirstObject(ps.getPropertyName(), context).getDateValue(
+ ps.getPropertyName());
+ } else {
+ return xdoc.getObject(ps.getClassName(), ps.getObjectIndex()).getDateValue(
+ ps.getPropertyName());
+ }
+ }
+
+ /**
+ * Retrieves the value of a list property.
+ */
+ protected List getListValue(String mapping, Document doc, XWikiContext context)
+ throws XWikiException
+ {
+ XWikiDocument xdoc = context.getWiki().getDocument(doc.getFullName(), context);
+ PropertySelector ps = new PropertySelector(mapping);
+ if (ps.getClassName() == null) {
+ return xdoc.getFirstObject(ps.getPropertyName(), context).getListValue(
+ ps.getPropertyName());
+ } else {
+ return xdoc.getObject(ps.getClassName(), ps.getObjectIndex()).getListValue(
+ ps.getPropertyName());
+ }
+ }
+
+ /**
+ * @return base + (extra - base)
+ */
+ protected Map joinParams(Map base, Map extra)
+ {
+ Map params = new HashMap();
+ params.putAll(base);
+ Iterator it = extra.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry entry = (Map.Entry) it.next();
+ if (params.get(entry.getKey()) == null) {
+ params.put(entry.getKey(), entry.getValue());
+ }
+ }
+ return params;
+ }
+
+ /**
+ * Cleans up the given XML fragment using the specified configuration.
+ *
+ * @param xmlFragment the XML fragment to be cleaned up
+ * @param config the configuration properties to use
+ * @return the DOM tree associated with the cleaned up XML fragment
+ */
+ public static org.w3c.dom.Document tidy(String xmlFragment, Properties config)
+ {
+ Tidy tidy = new Tidy();
+ tidy.setConfigurationFromProps(config);
+ return tidy.parseDOM(new ByteArrayInputStream(xmlFragment.getBytes()),
+ (OutputStream) null);
+ }
+
+ /**
+ * Computes the sum of lengths of all the text nodes within the given DOM sub-tree
+ *
+ * @param node the root of the DOM sub-tree containing the text
+ * @return the sum of lengths of text nodes within the given DOM sub-tree
+ */
+ public static int innerTextLength(Node node)
+ {
+ switch (node.getNodeType()) {
+ case Node.TEXT_NODE:
+ return node.getNodeValue().length();
+ case Node.ELEMENT_NODE:
+ int length = 0;
+ Node child = node.getFirstChild();
+ while (child != null) {
+ length += innerTextLength(child);
+ child = child.getNextSibling();
+ }
+ return length;
+ case Node.DOCUMENT_NODE:
+ return innerTextLength(((org.w3c.dom.Document) node).getDocumentElement());
+ default:
+ return 0;
+ }
+ }
+
+ /**
+ * Extracts a well-formed XML fragment from the given DOM tree.
+ *
+ * @param node the root of the DOM tree where the extraction takes place
+ * @param start the index of the first character
+ * @param length the maximum number of characters in text nodes to include in the returned
+ * fragment
+ * @return a well-formed XML fragment starting at the given character index and having up to the
+ * specified length, summing only the characters in text nodes
+ */
+ public static String extractXML(Node node, int start, int length) throws XWikiException
+ {
+ ExtractHandler handler = null;
+ try {
+ handler = new ExtractHandler(start, length);
+ Transformer xformer = TransformerFactory.newInstance().newTransformer();
+ xformer.transform(new DOMSource(node), new SAXResult(handler));
+ return handler.getResult();
+ } catch (Throwable t) {
+ if (handler != null && handler.isFinished()) {
+ return handler.getResult();
+ } else {
+ throw new XWikiException();
+ }
+ }
+ }
+
+ /**
+ * Extracts the first characters of the given XML fragment, up to the given length limit, adding
+ * only characters in XML text nodes. The XML fragment is cleaned up before extracting the
+ * prefix to be sure that the result is well-formed.
+ *
+ * @param xmlFragment the full XML text
+ * @param previewLength the maximum number of characters allowed in the preview, considering
+ * only the XML text nodes
+ * @return a prefix of the given XML fragment summing at most previewLength
+ * characters in its text nodes
+ */
+ public static String getXMLPreview(String xmlFragment, int previewLength)
+ {
+ try {
+ return extractXML(tidy(xmlFragment, TIDY_XML_CONFIG), 0, previewLength);
+ } catch (XWikiException e) {
+ return getPlainPreview(xmlFragment, previewLength);
+ }
+ }
+
+ /**
+ * Extracts the first characters of the given HTML fragment, up to the given length limit,
+ * adding only characters in HTML text nodes. The HTML fragment is cleaned up before extracting
+ * the prefix to be sure the result is well-formed.
+ *
+ * @param htmlFragment the full HTML text
+ * @param previewLength the maximum number of characters allowed in the preview, considering
+ * only the HTML text nodes
+ * @return a prefix of the given HTML fragment summing at most previewLength
+ * characters in its text nodes
+ */
+ public static String getHTMLPreview(String htmlFragment, int previewLength)
+ {
+ try {
+ org.w3c.dom.Document html = tidy(htmlFragment, TIDY_HTML_CONFIG);
+ Node body = html.getElementsByTagName("body").item(0);
+ return extractXML(body.getFirstChild(), 0, previewLength);
+ } catch (XWikiException e) {
+ return getPlainPreview(htmlFragment, previewLength);
+ }
+ }
+
+ /**
+ * Extracts the first characters of the given text, up to the last space within the given length
+ * limit.
+ *
+ * @param plainText the full text
+ * @param previewLength the maximum number of characters allowed in the preview
+ * @return a prefix of the plainText
having at most previewLength
+ * characters
+ */
+ public static String getPlainPreview(String plainText, int previewLength)
+ {
+ if (plainText.length() <= previewLength) {
+ return plainText;
+ }
+ // We remove the leading and trailing spaces from the given text to avoid interfering
+ // with the last space within the length limit
+ plainText = plainText.trim();
+ if (plainText.length() <= previewLength) {
+ return plainText;
+ }
+ int spaceIndex = plainText.lastIndexOf(" ", previewLength);
+ if (spaceIndex < 0) {
+ spaceIndex = previewLength;
+ }
+ plainText = plainText.substring(0, spaceIndex);
+ return plainText;
+ }
+
+ /**
+ * Creates a new {@link SyndContent} instance for the given content type and value.
+ *
+ * @param type content type
+ * @param value the content
+ * @return a new {@link SyndContent} instance
+ */
+ protected SyndContent getSyndContent(String type, String value)
+ {
+ SyndContent content = new SyndContentImpl();
+ content.setType(type);
+ content.setValue(value);
+ return content;
+ }
+
+ /**
+ * Casts the given object to a {@link Document} instance. The given object must be either a
+ * {@link Document} instance already, a {@link XWikiDocument} instance or the full name of the
+ * document.
+ *
+ * @param obj object to be casted
+ * @param context the XWiki context
+ * @return the document associated with the given object
+ * @throws XWikiException if the given object is neither a {@link Document} instance, a
+ * {@link XWikiDocument} instance nor the full name of the document
+ */
+ protected Document castDocument(Object obj, XWikiContext context) throws XWikiException
+ {
+ if (obj instanceof Document) {
+ return (Document) obj;
+ } else if (obj instanceof XWikiDocument) {
+ return ((XWikiDocument) obj).newDocument(context);
+ } else if (obj instanceof String) {
+ return context.getWiki().getDocument((String) obj, context).newDocument(context);
+ } else {
+ throw new XWikiException(XWikiException.MODULE_XWIKI_PLUGINS,
+ XWikiException.ERROR_XWIKI_DOES_NOT_EXIST,
+ "");
+ }
+ }
+}
Index: xwiki-platform-core/xwiki-core/src/main/java/com/xpn/xwiki/plugin/feed/SyndEntrySource.java
===================================================================
--- xwiki-platform-core/xwiki-core/src/main/java/com/xpn/xwiki/plugin/feed/SyndEntrySource.java (revision 0)
+++ xwiki-platform-core/xwiki-core/src/main/java/com/xpn/xwiki/plugin/feed/SyndEntrySource.java (revision 0)
@@ -0,0 +1,46 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package com.xpn.xwiki.plugin.feed;
+
+import java.util.Map;
+
+import com.sun.syndication.feed.synd.SyndEntry;
+import com.xpn.xwiki.XWikiContext;
+import com.xpn.xwiki.XWikiException;
+
+/**
+ * Abstracts a strategy for computing the field values of a feed entry from a generic source.
+ */
+public interface SyndEntrySource
+{
+ /**
+ * Overwrites the current values of the given feed entry with new ones computed from the
+ * specified source object.
+ *
+ * @param entry the feed entry whose fields are going to be overwritten
+ * @param obj the source for the new values to be set on the fields of the feed entry
+ * @param params parameters to adjust the computation. Each concrete strategy may define its own
+ * (key, value) pairs
+ * @param context the XWiki context
+ * @throws XWikiException
+ */
+ void source(SyndEntry entry, Object obj, Map params, XWikiContext context)
+ throws XWikiException;
+}
Index: xwiki-platform-core/xwiki-core/src/main/java/com/xpn/xwiki/plugin/feed/FeedPlugin.java
===================================================================
--- xwiki-platform-core/xwiki-core/src/main/java/com/xpn/xwiki/plugin/feed/FeedPlugin.java (revision 9638)
+++ xwiki-platform-core/xwiki-core/src/main/java/com/xpn/xwiki/plugin/feed/FeedPlugin.java (working copy)
@@ -22,17 +22,22 @@
package com.xpn.xwiki.plugin.feed;
import java.io.IOException;
+import java.io.StringWriter;
+import java.lang.reflect.Constructor;
import java.net.URL;
import java.util.*;
import com.sun.syndication.feed.synd.SyndCategory;
import com.sun.syndication.feed.synd.SyndContent;
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.feed.synd.SyndImage;
+import com.sun.syndication.feed.synd.SyndImageImpl;
+import com.sun.syndication.io.SyndFeedOutput;
import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.XWikiException;
-import com.xpn.xwiki.web.XWikiEngineContext;
import com.xpn.xwiki.api.Api;
import com.xpn.xwiki.cache.api.XWikiCache;
import com.xpn.xwiki.cache.api.XWikiCacheNeedsRefreshException;
@@ -705,4 +710,156 @@
return null;
}
+ /**
+ * @see FeedPluginApi#getSyndEntrySource(String, Map)
+ */
+ public SyndEntrySource getSyndEntrySource(String className, Map params, XWikiContext context)
+ throws XWikiException
+ {
+ try {
+ Class sesc = Class.forName(className).asSubclass(SyndEntrySource.class);
+ Constructor ctor = null;
+ if (params != null) {
+ try {
+ ctor = sesc.getConstructor(new Class[] {Map.class});
+ return (SyndEntrySource) ctor.newInstance(new Object[] {params});
+ } catch (Throwable t) {
+ }
+ }
+ ctor = sesc.getConstructor(new Class[] {});
+ return (SyndEntrySource) ctor.newInstance(new Object[] {});
+ } catch (Throwable t) {
+ throw new XWikiException(XWikiException.MODULE_XWIKI_PLUGINS,
+ XWikiException.ERROR_XWIKI_UNKNOWN,
+ "",
+ t);
+ }
+ }
+
+ /**
+ * @see FeedPluginApi#getFeedEntry()
+ */
+ public SyndEntry getFeedEntry(XWikiContext context)
+ {
+ return new SyndEntryImpl();
+ }
+
+ /**
+ * @see FeedPluginApi#getFeedImage()
+ */
+ public SyndImage getFeedImage(XWikiContext context)
+ {
+ return new SyndImageImpl();
+ }
+
+ /**
+ * @see FeedPluginApi#getFeed()
+ */
+ public SyndFeed getFeed(XWikiContext context)
+ {
+ return new SyndFeedImpl();
+ }
+
+ /**
+ * @see FeedPluginApi#getFeed(List, SyndEntrySourceApi, Map)
+ */
+ public SyndFeed getFeed(List list, SyndEntrySource source, Map sourceParams,
+ XWikiContext context) throws XWikiException
+ {
+ SyndFeed feed = getFeed(context);
+ List entries = new ArrayList();
+ for (int i = 0; i < list.size(); i++) {
+ SyndEntry entry = getFeedEntry(context);
+ try {
+ source.source(entry, list.get(i), sourceParams, context);
+ entries.add(entry);
+ } catch (Throwable t) {
+ // skip this entry
+ }
+ }
+ feed.setEntries(entries);
+ return feed;
+ }
+
+ /**
+ * @see FeedPluginApi#getFeed(String, int, int, SyndEntrySourceApi, Map)
+ */
+ public SyndFeed getFeed(String query, int count, int start, SyndEntrySource source,
+ Map sourceParams, XWikiContext context) throws XWikiException
+ {
+ List entries =
+ context.getWiki().getStore().searchDocumentsNames(query, count, start, context);
+ return getFeed(entries, source, sourceParams, context);
+ }
+
+ /**
+ * @see FeedPluginApi#getFeed(List, SyndEntrySourceApi, Map, Map)
+ */
+ public SyndFeed getFeed(List list, SyndEntrySource source, Map sourceParams, Map metadata,
+ XWikiContext context) throws XWikiException
+ {
+ SyndFeed feed = getFeed(list, source, sourceParams, context);
+ fillFeedMetadata(feed, metadata);
+ return feed;
+ }
+
+ /**
+ * @see FeedPluginApi#getFeed(String, int, int, SyndEntrySourceApi, Map, Map)
+ */
+ public SyndFeed getFeed(String query, int count, int start, SyndEntrySource source,
+ Map sourceParams, Map metadata, XWikiContext context) throws XWikiException
+ {
+ SyndFeed feed = getFeed(query, count, start, source, sourceParams, context);
+ fillFeedMetadata(feed, metadata);
+ return feed;
+ }
+
+ private void fillFeedMetadata(SyndFeed feed, Map metadata)
+ {
+ feed.setAuthor(String.valueOf(metadata.get("author")));
+ feed.setDescription(String.valueOf(metadata.get("description")));
+ feed.setCopyright(String.valueOf(metadata.get("copyright")));
+ feed.setEncoding(String.valueOf(metadata.get("encoding")));
+ feed.setLink(String.valueOf(metadata.get("url")));
+ feed.setTitle(String.valueOf(metadata.get("title")));
+ feed.setLanguage(String.valueOf(metadata.get("language")));
+ }
+
+ /**
+ * @see FeedPluginApi#getFeedOutput(SyndFeed, String)
+ */
+ public String getFeedOutput(SyndFeed feed, String type, XWikiContext context)
+ {
+ feed.setFeedType(type);
+ StringWriter writer = new StringWriter();
+ SyndFeedOutput output = new SyndFeedOutput();
+ try {
+ output.output(feed, writer);
+ writer.close();
+ return writer.toString();
+ } catch (Exception e) {
+ e.printStackTrace();
+ return "";
+ }
+ }
+
+ /**
+ * @see FeedPluginApi#getFeedOutput(List, SyndEntrySourceApi, Map, Map, String)
+ */
+ public String getFeedOutput(List list, SyndEntrySource source, Map sourceParams,
+ Map metadata, String type, XWikiContext context) throws XWikiException
+ {
+ SyndFeed feed = getFeed(list, source, sourceParams, metadata, context);
+ return getFeedOutput(feed, type, context);
+ }
+
+ /**
+ * @see FeedPluginApi#getFeedOutput(String, int, int, SyndEntrySourceApi, Map, Map, String)
+ */
+ public String getFeedOutput(String query, int count, int start, SyndEntrySource source,
+ Map sourceParams, Map metadata, String type, XWikiContext context) throws XWikiException
+ {
+ SyndFeed feed = getFeed(query, count, start, source, sourceParams, metadata, context);
+ return getFeedOutput(feed, type, context);
+ }
}
Index: xwiki-platform-core/xwiki-core/src/main/java/com/xpn/xwiki/plugin/feed/SyndEntrySourceApi.java
===================================================================
--- xwiki-platform-core/xwiki-core/src/main/java/com/xpn/xwiki/plugin/feed/SyndEntrySourceApi.java (revision 0)
+++ xwiki-platform-core/xwiki-core/src/main/java/com/xpn/xwiki/plugin/feed/SyndEntrySourceApi.java (revision 0)
@@ -0,0 +1,97 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package com.xpn.xwiki.plugin.feed;
+
+import java.util.Collections;
+import java.util.Map;
+
+import com.sun.syndication.feed.synd.SyndEntry;
+import com.sun.syndication.feed.synd.SyndEntryImpl;
+import com.xpn.xwiki.XWikiContext;
+import com.xpn.xwiki.XWikiException;
+import com.xpn.xwiki.api.Api;
+
+/**
+ * API for {@link SyndEntrySource}
+ */
+public class SyndEntrySourceApi extends Api
+{
+ public static final String SYND_ENTRY_SOURCE_EXCEPTION = "SyndEntrySourceException";
+
+ private SyndEntrySource source;
+
+ public SyndEntrySourceApi(SyndEntrySource source, XWikiContext context)
+ {
+ super(context);
+ this.source = source;
+ }
+
+ protected SyndEntrySource getSyndEntrySource()
+ {
+ return this.source;
+ }
+
+ /**
+ * @see SyndEntrySource#source(SyndEntry, Object, java.util.Map, XWikiContext)
+ */
+ public boolean source(SyndEntry entry, Object obj, Map params)
+ {
+ getXWikiContext().remove(SYND_ENTRY_SOURCE_EXCEPTION);
+ try {
+ this.source.source(entry, obj, params, getXWikiContext());
+ return true;
+ } catch (XWikiException e) {
+ getXWikiContext().put(SYND_ENTRY_SOURCE_EXCEPTION, e);
+ return false;
+ }
+ }
+
+ /**
+ * @see SyndEntrySource#source(SyndEntry, Object, java.util.Map, XWikiContext)
+ */
+ public boolean source(SyndEntry entry, Object obj)
+ {
+ return this.source(entry, obj, Collections.EMPTY_MAP);
+ }
+
+ /**
+ * @see SyndEntrySource#source(SyndEntry, Object, java.util.Map, XWikiContext)
+ */
+ public SyndEntry source(Object obj, Map params)
+ {
+ getXWikiContext().remove(SYND_ENTRY_SOURCE_EXCEPTION);
+ try {
+ SyndEntry entry = new SyndEntryImpl();
+ this.source.source(entry, obj, params, getXWikiContext());
+ return entry;
+ } catch (XWikiException e) {
+ getXWikiContext().put(SYND_ENTRY_SOURCE_EXCEPTION, e);
+ return null;
+ }
+ }
+
+ /**
+ * @see SyndEntrySource#source(SyndEntry, Object, java.util.Map, XWikiContext)
+ */
+ public SyndEntry source(Object obj)
+ {
+ return this.source(obj, Collections.EMPTY_MAP);
+ }
+}
Index: xwiki-platform-core/xwiki-core/src/main/java/com/xpn/xwiki/plugin/feed/FeedPluginApi.java
===================================================================
--- xwiki-platform-core/xwiki-core/src/main/java/com/xpn/xwiki/plugin/feed/FeedPluginApi.java (revision 9638)
+++ xwiki-platform-core/xwiki-core/src/main/java/com/xpn/xwiki/plugin/feed/FeedPluginApi.java (working copy)
@@ -23,13 +23,25 @@
import java.io.IOException;
import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.feed.synd.SyndFeed;
+import com.sun.syndication.feed.synd.SyndImage;
+import com.xpn.xwiki.XWiki;
import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.XWikiException;
import com.xpn.xwiki.api.Api;
+import com.xpn.xwiki.api.Document;
+import com.xpn.xwiki.doc.XWikiDocument;
+import com.xpn.xwiki.web.XWikiRequest;
public class FeedPluginApi extends Api {
+ public static final String FEED_PLUGIN_EXCEPTION = "FeedPluginException";
+
private FeedPlugin plugin;
public FeedPluginApi(FeedPlugin plugin, XWikiContext context) {
@@ -168,4 +180,714 @@
return plugin.getActiveUpdateThreads();
}
+ /**
+ * Tries to instantiate a class implementing the {@link SyndEntrySource} interface using the
+ * given parameters
+ *
+ * @param className the name of a class implementing {@link SyndEntrySource} interface
+ * @param params constructor parameters
+ * @return a new SyndEntrySource instance
+ */
+ public SyndEntrySourceApi getSyndEntrySource(String className, Map params)
+ {
+ getXWikiContext().remove(FEED_PLUGIN_EXCEPTION);
+ try {
+ SyndEntrySource source =
+ plugin.getSyndEntrySource(className, params, getXWikiContext());
+ return new SyndEntrySourceApi(source, getXWikiContext());
+ } catch (XWikiException e) {
+ getXWikiContext().put(FEED_PLUGIN_EXCEPTION, e);
+ return null;
+ }
+ }
+
+ /**
+ * @see #getSyndEntrySource(String, Map)
+ */
+ public SyndEntrySourceApi getSyndEntrySource(String className)
+ {
+ return this.getSyndEntrySource(className, null);
+ }
+
+ /**
+ * Instantiates the default strategy for converting documents in feed entries.
+ *
+ * @param params strategy parameters
+ * @return a new {@link SyndEntrySourceApi} wrapping a {@link SyndEntryDocumentSource} object
+ */
+ public SyndEntrySourceApi getSyndEntryDocumentSource(Map params)
+ {
+ return this.getSyndEntrySource(SyndEntryDocumentSource.class.getName());
+ }
+
+ /**
+ * @see #getSyndEntryDocumentSource(Map)
+ */
+ public SyndEntrySourceApi getSyndEntryDocumentSource()
+ {
+ return getSyndEntryDocumentSource(Collections.EMPTY_MAP);
+ }
+
+ /**
+ * Instantiates the default strategy for converting articles in feed entries.
+ *
+ * @return a new {@link SyndEntrySourceApi} wrapping a {@link SyndEntryArticleSource} object
+ */
+ public SyndEntrySourceApi getSyndEntryArticleSource()
+ {
+ return getSyndEntryArticleSource(Collections.EMPTY_MAP);
+ }
+
+ /**
+ * Instantiates the default strategy for converting articles in feed entries, allowing you to
+ * customize it through parameters.
+ *
+ * @return a new {@link SyndEntrySourceApi} wrapping a {@link SyndEntryArticleSource} object
+ */
+ public SyndEntrySourceApi getSyndEntryArticleSource(Map params)
+ {
+ Map defParams = new HashMap();
+ defParams.put(SyndEntryDocumentSource.FIELD_TITLE, "XWiki.ArticleClass_title");
+ defParams.put(SyndEntryDocumentSource.FIELD_DESCRIPTION, "XWiki.ArticleClass_content");
+ defParams.put(SyndEntryDocumentSource.FIELD_CATEGORIES, "XWiki.ArticleClass_category");
+ defParams.put(SyndEntryDocumentSource.CONTENT_LENGTH, new Integer(400));
+ defParams.putAll(params);
+ return this.getSyndEntrySource(SyndEntryDocumentSource.class.getName(), defParams);
+ }
+
+ /**
+ * Creates an empty feed entry
+ *
+ * @return a new feed entry
+ */
+ public SyndEntry getFeedEntry()
+ {
+ return plugin.getFeedEntry(getXWikiContext());
+ }
+
+ /**
+ * Creates an empty feed image
+ *
+ * @return a new feed image
+ */
+ public SyndImage getFeedImage()
+ {
+ return plugin.getFeedImage(context);
+ }
+
+ /**
+ * Creates a new feed image having the given properties.
+ *
+ * @param url image URL
+ * @param title image title
+ * @param description image description
+ * @return a new feed image
+ */
+ public SyndImage getFeedImage(String url, String link, String title, String description)
+ {
+ SyndImage image = getFeedImage();
+ image.setUrl(url);
+ image.setLink(link);
+ image.setTitle(title);
+ image.setDescription(description);
+ return image;
+ }
+
+ /**
+ * Creates a new instance of the default feed image. The default image file name is taken from
+ * the logo skin preference. If this preference is missing, logo.png is used
+ * instead.
+ *
+ * @return a new feed image
+ */
+ public SyndImage getDefaultFeedImage()
+ {
+ // Currently, getSkinFile method returns relative (internal) URLs. I couldn't find a way to
+ // get the external URL for a skin file. Is this something forbidden? I've noticed that we
+ // actually compute the full URL but we strip it with urlFactory.getURL(url, context). So
+ // what do you think of overloading the getSkinFile method by adding a absoluteURL flag?
+ XWiki xwiki = getXWikiContext().getWiki();
+ String fileName = xwiki.getSkinPreference("logo", "logo.png", getXWikiContext());
+ String url = xwiki.getSkinFile(fileName, getXWikiContext());
+ String port = "";
+ XWikiRequest request = getXWikiContext().getRequest();
+ if (("http".equals(request.getScheme()) && request.getServerPort() != 80)
+ || ("https".equals(request.getScheme()) && request.getServerPort() != 443)) {
+ port = ":" + request.getServerPort();
+ }
+ url = request.getScheme() + "://" + request.getServerName() + port + url;
+ String link = "http://" + request.getServerName();
+ return getFeedImage(url, link, "XWiki Logo", "XWiki Logo");
+ }
+
+ /**
+ * Creates an empty feed
+ *
+ * @return a new feed
+ */
+ public SyndFeed getFeed()
+ {
+ return plugin.getFeed(getXWikiContext());
+ }
+
+ /**
+ * Computes a new feed from a list of source items and a corresponding strategy for converting
+ * them in feed entries
+ *
+ * @param list the list of source items
+ * @param sourceApi the strategy to use for computing feed entries from source items
+ * @param sourceParams strategy parameters
+ * @return a new feed
+ */
+ public SyndFeed getFeed(List list, SyndEntrySourceApi sourceApi, Map sourceParams)
+ {
+ return getFeed(list, sourceApi, sourceParams, Collections.EMPTY_MAP);
+ }
+
+ /**
+ * Creates a new feed from a list of documents, using the default strategy for converting
+ * documents in feed entries. You can customize this strategy using strategy parameters.
+ *
+ * @param list a list of {@link Document} objects, {@link XWikiDocument} objects or document
+ * names
+ * @param params strategy parameters
+ * @return a new feed
+ * @see Document
+ * @see #getFeed(List, SyndEntrySourceApi, Map)
+ * @see SyndEntryDocumentSource
+ */
+ public SyndFeed getDocumentFeed(List list, Map params)
+ {
+ return getDocumentFeed(list, params, Collections.EMPTY_MAP);
+ }
+
+ /**
+ * Creates a new feed from a list of articles, using the default strategy for converting
+ * articles in feed entries. By articles we mean any document containing an
+ * XWiki.ArticleClass
object.
+ *
+ * @param list a list of articles
+ * @return a new feed
+ * @see Document
+ * @see #getFeed(List, SyndEntrySourceApi, Map)
+ * @see SyndEntryArticleSource
+ */
+ public SyndFeed getArticleFeed(List list)
+ {
+ return getArticleFeed(list, Collections.EMPTY_MAP);
+ }
+
+ /**
+ * Instantiates the default document feed.
+ *
+ * @param list a list of {@link Document} objects, {@link XWikiDocument} objects or document
+ * names
+ * @return a new feed
+ * @see #getDocumentFeed(List)
+ */
+ public SyndFeed getWebFeed(List list)
+ {
+ return getWebFeed(list, Collections.EMPTY_MAP);
+ }
+
+ /**
+ * Instantiates the default article feed.
+ *
+ * @param list a list of articles (as document instances or document names)
+ * @return a new feed
+ * @see #getArticleFeed(List)
+ */
+ public SyndFeed getBlogFeed(List list)
+ {
+ return getBlogFeed(list, Collections.EMPTY_MAP);
+ }
+
+ /**
+ * Creates a new feed from the result of an HQL query and a corresponding strategy for
+ * converting the retrieved documents in feed entries.
+ *
+ * @param query the HQL query used for retrieving the documents
+ * @param count the maximum number of documents to retrieve
+ * @param start the start index
+ * @param sourceApi the strategy to use for computing feed entries from source items
+ * @param sourceParams strategy parameters
+ * @return a new feed
+ */
+ public SyndFeed getFeed(String query, int count, int start, SyndEntrySourceApi sourceApi,
+ Map sourceParams)
+ {
+ return getFeed(query, count, start, sourceApi, sourceParams, Collections.EMPTY_MAP);
+ }
+
+ /**
+ * Creates a new feed from the result of an HQL query, using the default strategy for converting
+ * documents in feed entries. You can customize this strategy using strategy parameters.
+ *
+ * @param query the HQL query used for retrieving the documents
+ * @param count the maximum number of documents to retrieve
+ * @param start the start index
+ * @param params strategy parameters
+ * @return a new feed
+ * @see Document
+ * @see #getFeed(String, int, int, SyndEntrySourceApi, Map)
+ * @see SyndEntryDocumentSource
+ */
+ public SyndFeed getDocumentFeed(String query, int count, int start, Map params)
+ {
+ return getDocumentFeed(query, count, start, params, Collections.EMPTY_MAP);
+ }
+
+ /**
+ * Creates a new feed from the result of an HQL query, using the default strategy for converting
+ * articles in feed entries. By articles we mean any document containing a
+ * XWiki.ArticleClass
object.
+ *
+ * @param query the HQL query used for retrieving the articles
+ * @param count the maximum number of articles to retrieve
+ * @param start the start index
+ * @return a new feed
+ * @see Document
+ * @see #getFeed(String, int, int, SyndEntrySourceApi, Map)
+ * @see SyndEntryArticleSource
+ */
+ public SyndFeed getArticleFeed(String query, int count, int start)
+ {
+ return getArticleFeed(query, count, start, Collections.EMPTY_MAP);
+ }
+
+ /**
+ * Instantiates the default document feed.
+ *
+ * @param query the HQL query used for retrieving the documents
+ * @param count the maximum number of documents to retrieve
+ * @param start the start index
+ * @return a new feed
+ * @see #getDocumentFeed(String, int, int)
+ */
+ public SyndFeed getWebFeed(String query, int count, int start)
+ {
+ return getWebFeed(query, count, start, Collections.EMPTY_MAP);
+ }
+
+ /**
+ * Instantiates the default article feed.
+ *
+ * @param query the HQL query used for retrieving the articles
+ * @param count the maximum number of articles to retrieve
+ * @param start the start index
+ * @return a new feed
+ * @see #getArticleFeed(String, int, int)
+ */
+ public SyndFeed getBlogFeed(String query, int count, int start)
+ {
+ return getBlogFeed(query, count, start, Collections.EMPTY_MAP);
+ }
+
+ /**
+ * Computes a new feed from a list of source items and a corresponding strategy for converting
+ * them in feed entries, filling in the feed meta data.
+ *
+ * @param list the list of source items
+ * @param sourceApi the strategy to use for computing feed entries from source items
+ * @param sourceParams strategy parameters
+ * @param metadata feed meta data (includes the author, description, copyright, encoding, url,
+ * title)
+ * @return a new feed
+ */
+ public SyndFeed getFeed(List list, SyndEntrySourceApi sourceApi, Map sourceParams,
+ Map metadata)
+ {
+ getXWikiContext().remove(FEED_PLUGIN_EXCEPTION);
+ try {
+ return plugin.getFeed(list, sourceApi.getSyndEntrySource(), sourceParams, metadata,
+ getXWikiContext());
+ } catch (XWikiException e) {
+ getXWikiContext().put(FEED_PLUGIN_EXCEPTION, e);
+ return null;
+ }
+ }
+
+ /**
+ * Creates a new feed from a list of documents, using the default strategy for converting
+ * documents in feed entries, filling in the feed meta data. You can customize the default
+ * strategy by using strategy parameters.
+ *
+ * @param list a list of {@link Document} objects, {@link XWikiDocument} objects or document
+ * names
+ * @param params strategy parameters
+ * @param metadata feed meta data (includes the author, description, copyright, encoding, url,
+ * title)
+ * @return a new feed
+ * @see Document
+ * @see #getFeed(List, SyndEntrySourceApi, Map, Map)
+ * @see SyndEntryDocumentSource
+ */
+ public SyndFeed getDocumentFeed(List list, Map params, Map metadata)
+ {
+ return getFeed(list, getSyndEntryDocumentSource(), params, metadata);
+ }
+
+ /**
+ * Creates a new feed from a list of articles, using the default strategy for converting
+ * articles in feed entries, filling in the feed meta data. By articles we mean any document
+ * containing an XWiki.ArticleClass
object.
+ *
+ * @param list a list of articles
+ * @param metadata feed meta data (includes the author, description, copyright, encoding, url,
+ * title)
+ * @return a new feed
+ * @see Document
+ * @see #getFeed(List, SyndEntrySourceApi, Map, Map)
+ * @see SyndEntryArticleSource
+ */
+ public SyndFeed getArticleFeed(List list, Map metadata)
+ {
+ return getFeed(list, getSyndEntryArticleSource(), Collections.EMPTY_MAP, metadata);
+ }
+
+ private static boolean keyHasValue(Map map, Object key, Object defaultValue)
+ {
+ Object value = map.get(key);
+ return value != null && !value.equals(defaultValue);
+ }
+
+ /**
+ * Fills the missing feed meta data fields with default values.
+ */
+ private Map fillDefaultFeedMetadata(Map metadata)
+ {
+ XWiki xwiki = getXWikiContext().getWiki();
+ XWikiDocument doc = getXWikiContext().getDoc();
+ if (metadata.get("author") == null) {
+ metadata.put("author", xwiki.getUserName(doc.getAuthor(), null, false,
+ getXWikiContext()));
+ }
+ if (!keyHasValue(metadata, "copyright", "")) {
+ metadata.put("copyright", xwiki.getWebCopyright(getXWikiContext()));
+ }
+ if (!keyHasValue(metadata, "encoding", "")) {
+ metadata.put("encoding", xwiki.getEncoding());
+ }
+ if (!keyHasValue(metadata, "url", "")) {
+ metadata.put("url", "http://" + getXWikiContext().getRequest().getServerName());
+ }
+ if (!keyHasValue(metadata, "language", "")) {
+ metadata.put("language", doc.getDefaultLanguage());
+ }
+ return metadata;
+ }
+
+ private Map fillWebFeedMetadata(Map metadata)
+ {
+ fillDefaultFeedMetadata(metadata);
+ // these strings should be taken from a resource bundle
+ String title = "Feed for document changes";
+ String description = title;
+ if (!keyHasValue(metadata, "title", "")) {
+ metadata.put("title", title);
+ }
+ if (!keyHasValue(metadata, "description", "")) {
+ metadata.put("description", description);
+ }
+ return metadata;
+ }
+
+ private Map fillBlogFeedMetadata(Map metadata)
+ {
+ fillDefaultFeedMetadata(metadata);
+ // these strings should be taken from a resource bundle
+ String title = "Personal Wiki Blog";
+ String description = title;
+ if (!keyHasValue(metadata, "title", "")) {
+ metadata.put("title", title);
+ }
+ if (!keyHasValue(metadata, "description", "")) {
+ metadata.put("description", description);
+ }
+ return metadata;
+ }
+
+ /**
+ * Instantiates the default document feed.
+ *
+ * @param list a list of {@link Document} objects, {@link XWikiDocument} objects or document
+ * names
+ * @param metadata feed meta data (includes the author, description, copyright, encoding, url,
+ * title)
+ * @return a new feed
+ * @see #getDocumentFeed(List, Map)
+ */
+ public SyndFeed getWebFeed(List list, Map metadata)
+ {
+ SyndFeed webFeed = getDocumentFeed(list, Collections.EMPTY_MAP, fillWebFeedMetadata(metadata));
+ if (webFeed != null) {
+ webFeed.setImage(getDefaultFeedImage());
+ }
+ return webFeed;
+ }
+
+ /**
+ * Instantiates the default article feed.
+ *
+ * @param list a list of articles (as document instances or document names)
+ * @param metadata feed meta data (includes the author, description, copyright, encoding, url,
+ * title)
+ * @return a new feed
+ * @see #getArticleFeed(List, Map)
+ */
+ public SyndFeed getBlogFeed(List list, Map metadata)
+ {
+ SyndFeed blogFeed = getArticleFeed(list, fillBlogFeedMetadata(metadata));
+ if (blogFeed != null) {
+ blogFeed.setImage(getDefaultFeedImage());
+ }
+ return blogFeed;
+ }
+
+ /**
+ * Creates a new feed from the result of an HQL query and a corresponding strategy for
+ * converting the retrieved documents in feed entries, filling in the feed meta data.
+ *
+ * @param query the HQL query used for retrieving the documents
+ * @param count the maximum number of documents to retrieve
+ * @param start the start index
+ * @param sourceApi the strategy to use for computing feed entries from source items
+ * @param sourceParams strategy parameters
+ * @param metadata feed meta data (includes the author, description, copyright, encoding, url,
+ * title)
+ * @return a new feed
+ */
+ public SyndFeed getFeed(String query, int count, int start, SyndEntrySourceApi sourceApi,
+ Map sourceParams, Map metadata)
+ {
+ getXWikiContext().remove(FEED_PLUGIN_EXCEPTION);
+ try {
+ return plugin.getFeed(query, count, start, sourceApi.getSyndEntrySource(),
+ sourceParams, metadata, getXWikiContext());
+ } catch (XWikiException e) {
+ getXWikiContext().put(FEED_PLUGIN_EXCEPTION, e);
+ return null;
+ }
+ }
+
+ /**
+ * Creates a new feed from the result of an HQL query, using the default strategy for converting
+ * documents in feed entries, filling in the feed meta data. You can customize the default
+ * strategy by using strategy parameters.
+ *
+ * @param query the HQL query used for retrieving the documents
+ * @param count the maximum number of documents to retrieve
+ * @param start the start index
+ * @param params strategy parameters
+ * @param metadata feed meta data (includes the author, description, copyright, encoding, url,
+ * title)
+ * @return a new feed
+ * @see Document
+ * @see #getFeed(String, int, int, SyndEntrySourceApi, Map, Map)
+ * @see SyndEntryDocumentSource
+ */
+ public SyndFeed getDocumentFeed(String query, int count, int start, Map params, Map metadata)
+ {
+ return getFeed(query, count, start, getSyndEntryDocumentSource(), params, metadata);
+ }
+
+ /**
+ * Creates a new feed from the result of an HQL query, using the default strategy for converting
+ * articles in feed entries, filling in the feed meta data. By articles we mean any document
+ * containing a XWiki.ArticleClass
object.
+ *
+ * @param query the HQL query used for retrieving the articles
+ * @param count the maximum number of articles to retrieve
+ * @param start the start index
+ * @param metadata feed meta data (includes the author, description, copyright, encoding, url,
+ * title)
+ * @return a new feed
+ * @see Document
+ * @see #getFeed(String, int, int, SyndEntrySourceApi, Map, Map)
+ * @see SyndEntryArticleSource
+ */
+ public SyndFeed getArticleFeed(String query, int count, int start, Map metadata)
+ {
+ return getFeed(query, count, start, getSyndEntryArticleSource(), Collections.EMPTY_MAP,
+ metadata);
+ }
+
+ /**
+ * Instantiates the default document feed.
+ *
+ * @param query the HQL query used for retrieving the documents
+ * @param count the maximum number of documents to retrieve
+ * @param start the start index
+ * @param metadata feed meta data (includes the author, description, copyright, encoding, url,
+ * title)
+ * @return a new feed
+ * @see #getDocumentFeed(String, int, int, Map)
+ */
+ public SyndFeed getWebFeed(String query, int count, int start, Map metadata)
+ {
+ if (query == null) {
+ query = "where 1=1 order by doc.date desc";
+ }
+ SyndFeed webFeed = getDocumentFeed(query, count, start, Collections.EMPTY_MAP, fillWebFeedMetadata(metadata));
+ if (webFeed != null) {
+ webFeed.setImage(getDefaultFeedImage());
+ }
+ return webFeed;
+ }
+
+ /**
+ * Instantiates the default article feed.
+ *
+ * @param query the HQL query used for retrieving the articles
+ * @param count the maximum number of articles to retrieve
+ * @param start the start index
+ * @param metadata feed meta data (includes the author, description, copyright, encoding, url,
+ * title)
+ * @return a new feed
+ * @see #getArticleFeed(String, int, int, Map)
+ */
+ public SyndFeed getBlogFeed(String query, int count, int start, Map metadata)
+ {
+ if (query == null) {
+ XWikiRequest request = getXWikiContext().getRequest();
+ String category = request.getParameter("category");
+ if (category == null || category.equals("")) {
+ query =
+ ", BaseObject as obj where obj.name=doc.fullName and obj.className='XWiki.ArticleClass' and obj.name<>'XWiki.ArticleClassTemplate' order by doc.creationDate desc";
+ } else {
+ query =
+ ", BaseObject as obj, DBStringListProperty as prop join prop.list list where obj.name=doc.fullName and obj.className='XWiki.ArticleClass' and obj.name<>'XWiki.ArticleClassTemplate' and obj.id=prop.id.id and prop.id.name='category' and list = '"
+ + category + "' order by doc.creationDate desc";
+ }
+ }
+ SyndFeed blogFeed = getArticleFeed(query, count, start, fillBlogFeedMetadata(metadata));
+ if (blogFeed != null) {
+ blogFeed.setImage(getDefaultFeedImage());
+ }
+ return blogFeed;
+ }
+
+ /**
+ * Converts a feed into its string representation using the specified syntax
+ *
+ * @param feed any type of feed, implementing the {@link SyndFeed} interface
+ * @param type the feed type (syntax) to use, null if none. It can be any version of RSS
+ * or Atom. Some possible values are "rss_1.0", "rss_2.0" and "atom_1.0"
+ * @return the string representation of the given feed using the syntax associated with the
+ * specified feed type
+ */
+ public String getFeedOutput(SyndFeed feed, String type)
+ {
+ return plugin.getFeedOutput(feed, type, getXWikiContext());
+ }
+
+ /**
+ * @see #getFeedOutput(SyndFeed, String)
+ * @see #getFeed(List, SyndEntrySourceApi, Map, Map)
+ */
+ public String getFeedOutput(List list, SyndEntrySourceApi sourceApi, Map sourceParams,
+ Map metadata, String type)
+ {
+ getXWikiContext().remove(FEED_PLUGIN_EXCEPTION);
+ try {
+ return plugin.getFeedOutput(list, sourceApi.getSyndEntrySource(), sourceParams,
+ metadata, type, getXWikiContext());
+ } catch (XWikiException e) {
+ getXWikiContext().put(FEED_PLUGIN_EXCEPTION, e);
+ return null;
+ }
+ }
+
+ /**
+ * @see #getFeedOutput(List, SyndEntrySourceApi, Map, Map, String)
+ * @see SyndEntryDocumentSource
+ */
+ public String getDocumentFeedOutput(List list, Map params, Map metadata, String type)
+ {
+ return getFeedOutput(list, getSyndEntryDocumentSource(), params, metadata, type);
+ }
+
+ /**
+ * @see #getFeedOutput(List, SyndEntrySourceApi, Map, Map, String)
+ * @see SyndEntryArticleSource
+ */
+ public String getArticleFeedOutput(List list, Map metadata, String type)
+ {
+ return getFeedOutput(list, getSyndEntryArticleSource(), Collections.EMPTY_MAP, metadata,
+ type);
+ }
+
+ /**
+ * @see #getWebFeed(List, Map)
+ * @see #getFeedOutput(SyndFeed, String)
+ */
+ public String getWebFeedOutput(List list, Map metadata, String type)
+ {
+ return getFeedOutput(getWebFeed(list, metadata), type);
+ }
+
+ /**
+ * @see #getBlogFeed(List, Map)
+ * @see #getFeedOutput(SyndFeed, String)
+ */
+ public String getBlogFeedOutput(List list, Map metadata, String type)
+ {
+ return getFeedOutput(getBlogFeed(list, metadata), type);
+ }
+
+ /**
+ * @see #getFeedOutput(SyndFeed, String)
+ * @see #getFeed(String, int, int, SyndEntrySourceApi, Map, Map)
+ */
+ public String getFeedOutput(String query, int count, int start, SyndEntrySourceApi sourceApi,
+ Map sourceParams, Map metadata, String type)
+ {
+ getXWikiContext().remove(FEED_PLUGIN_EXCEPTION);
+ try {
+ return plugin.getFeedOutput(query, count, start, sourceApi.getSyndEntrySource(),
+ sourceParams, metadata, type, getXWikiContext());
+ } catch (XWikiException e) {
+ getXWikiContext().put(FEED_PLUGIN_EXCEPTION, e);
+ return null;
+ }
+ }
+
+ /**
+ * @see #getFeedOutput(String, int, int, SyndEntrySourceApi, Map, Map, String)
+ * @see SyndEntryDocumentSource
+ */
+ public String getDocumentFeedOutput(String query, int count, int start, Map params, Map metadata,
+ String type)
+ {
+ return getFeedOutput(query, count, start, getSyndEntryDocumentSource(), params, metadata, type);
+ }
+
+ /**
+ * @see #getFeedOutput(String, int, int, SyndEntrySourceApi, Map, Map, String)
+ * @see SyndEntryArticleSource
+ */
+ public String getArticleFeedOutput(String query, int count, int start, Map metadata,
+ String type)
+ {
+ return getFeedOutput(query, count, start, getSyndEntryArticleSource(),
+ Collections.EMPTY_MAP, metadata, type);
+ }
+
+ /**
+ * @see #getWebFeed(String, int, int, Map)
+ * @see #getFeedOutput(SyndFeed, String)
+ */
+ public String getWebFeedOutput(String query, int count, int start, Map metadata, String type)
+ {
+ return getFeedOutput(getWebFeed(query, count, start, metadata), type);
+ }
+
+ /**
+ * @see #getBlogFeed(String, int, int, Map)
+ * @see #getFeedOutput(SyndFeed, String)
+ */
+ public String getBlogFeedOutput(String query, int count, int start, Map metadata, String type)
+ {
+ return getFeedOutput(getBlogFeed(query, count, start, metadata), type);
+ }
}
Index: xwiki-platform-core/xwiki-core/src/main/java/com/xpn/xwiki/util/ExtractHandler.java
===================================================================
--- xwiki-platform-core/xwiki-core/src/main/java/com/xpn/xwiki/util/ExtractHandler.java (revision 0)
+++ xwiki-platform-core/xwiki-core/src/main/java/com/xpn/xwiki/util/ExtractHandler.java (revision 0)
@@ -0,0 +1,248 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package com.xpn.xwiki.util;
+
+import java.util.Iterator;
+import java.util.Stack;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+/**
+ * Extracts a well-formed XML fragment by listening to SAX events.
+ */
+public class ExtractHandler extends DefaultHandler
+{
+ private static class XMLTag
+ {
+ private String qName;
+
+ private Attributes atts;
+
+ public XMLTag(String qName, Attributes atts)
+ {
+ this.qName = qName;
+ this.atts = atts;
+ }
+
+ public String getQName()
+ {
+ return qName;
+ }
+
+ public Attributes getAtts()
+ {
+ return atts;
+ }
+ }
+
+ /**
+ * the number of characters, in text nodes, that have to be read before starting the extraction
+ */
+ private int lowerBound;
+
+ /**
+ * the maximum number of characters that may be read during the parsing process
+ */
+ private int upperBound;
+
+ /**
+ * the number of characters read so far
+ */
+ private int counter;
+
+ /**
+ * the stack of open tags; when the lower bound is reached all the tags in the stack must be
+ * opened; when the upper bound is reached all the tags in the stack must be closed.
+ */
+ private Stack openTags = new Stack();
+
+ /**
+ * the fragment that is extracted during the parsing process
+ */
+ private StringBuffer result;
+
+ /**
+ * true
if the extraction was successful. The parsing process throws an exception
+ * when the upper bound is reached; this flag is useful to distinguish between this exception
+ * and the others.
+ */
+ private boolean finished = false;
+
+ public ExtractHandler(int start, int length) throws SAXException
+ {
+ super();
+ if (start < 0) {
+ throw new SAXException("start must be greater than or equal to 0");
+ }
+ if (length <= 0) {
+ throw new SAXException("length must be greater than 0");
+ }
+ lowerBound = start;
+ upperBound = lowerBound + length;
+ }
+
+ public String getResult()
+ {
+ return result.toString();
+ }
+
+ public boolean isFinished()
+ {
+ return finished;
+ }
+
+ private void openTag(String qName, Attributes atts)
+ {
+ result.append("<" + qName);
+ for (int i = 0; i < atts.getLength(); i++) {
+ result.append(" " + atts.getQName(i) + "=\"" + atts.getValue(i) + "\"");
+ }
+ result.append(">");
+ }
+
+ private void openTags()
+ {
+ Iterator it = openTags.iterator();
+ while (it.hasNext()) {
+ XMLTag tag = (XMLTag) it.next();
+ openTag(tag.getQName(), tag.getAtts());
+ }
+ }
+
+ private void closeTags()
+ {
+ while (!openTags.isEmpty()) {
+ XMLTag tag = (XMLTag) openTags.pop();
+ closeTag(tag.getQName());
+ }
+ }
+
+ private void closeTag(String qName)
+ {
+ result.append("" + qName + ">");
+ }
+
+ private boolean isExtracting()
+ {
+ return lowerBound <= counter && counter <= upperBound;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see DefaultHandler#startDocument()
+ */
+ public void startDocument() throws SAXException
+ {
+ super.startDocument();
+ counter = 0;
+ openTags.clear();
+ result = new StringBuffer();
+ finished = false;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see DefaultHandler#startElement(String, String, String, Attributes)
+ */
+ public void startElement(String namespaceURI, String localName, String qName, Attributes atts)
+ throws SAXException
+ {
+ openTags.push(new XMLTag(qName, atts));
+ if (isExtracting()) {
+ openTag(qName, atts);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see DefaultHandler#characters(char[], int, int)
+ */
+ public void characters(char[] ch, int start, int length) throws SAXException
+ {
+ if (counter < lowerBound) {
+ if (counter + length < lowerBound) {
+ counter += length;
+ return;
+ } else {
+ start += lowerBound - counter;
+ length -= lowerBound - counter;
+ counter = lowerBound;
+ openTags();
+ }
+ }
+ int remainingLength = upperBound - counter;
+ if (remainingLength <= length) {
+ String content = new String(ch, start, remainingLength);
+ int spaceIndex = remainingLength;
+ if (remainingLength == length || ch[remainingLength] != ' ') {
+ spaceIndex = content.lastIndexOf(" ");
+ }
+ if (spaceIndex >= 0) {
+ counter += spaceIndex;
+ result.append(content.substring(0, spaceIndex));
+ } else {
+ counter = upperBound;
+ result.append(content);
+ }
+ endDocument();
+ throw new SAXException("length limit reached");
+ } else {
+ counter += length;
+ result.append(ch, start, length);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see DefaultHandler#endElement(String, String, String)
+ */
+ public void endElement(String namespaceURI, String localName, String qName)
+ throws SAXException
+ {
+ // We assume the XML fragment is well defined, and thus we shouldn't have a closed tag
+ // without its pair open tag. So we don't test for empty stack or tag match.
+ openTags.pop();
+ if (isExtracting()) {
+ closeTag(qName);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see DefaultHandler#endDocument()
+ */
+ public void endDocument() throws SAXException
+ {
+ super.endDocument();
+ // Close open tags
+ if (isExtracting()) {
+ closeTags();
+ }
+ // set finished flag to distinguish between "length limit reached" and other exceptions
+ finished = true;
+ }
+}