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,45 @@ +/* + * 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; + +/** + * Unit tests for {@link SyndEntryDocumentSource} + */ +public class SyndEntryDocumentSourceTest extends AbstractSyndEntrySourceTest +{ + protected void setUp() throws Exception + { + super.setUp(); + + source = new SyndEntryDocumentSource(); + } + + /** + * {@inheritDoc} + * + * @see AbstractSyndEntrySourceTest#testSourceContentType() + */ + public void testSourceContentType() + { + // We skip this test because SyndEntryDocumentSource currently supports only text/plain + // content type + assertTrue(true); + } +} Index: xwiki-platform-core/xwiki-core/src/test/java/com/xpn/xwiki/plugin/feed/AbstractSyndEntrySourceTest.java =================================================================== --- xwiki-platform-core/xwiki-core/src/test/java/com/xpn/xwiki/plugin/feed/AbstractSyndEntrySourceTest.java (revision 0) +++ xwiki-platform-core/xwiki-core/src/test/java/com/xpn/xwiki/plugin/feed/AbstractSyndEntrySourceTest.java (revision 0) @@ -0,0 +1,239 @@ +/* + * 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.Collections; +import java.util.HashMap; +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.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; + +/** + * This class provides a framework for writing tests and some basic tests for any class extending + * {@link AbstractSyndEntrySource}. + */ +public abstract class AbstractSyndEntrySourceTest 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"; + + protected AbstractSyndEntrySource 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.."); + context.getWiki().saveDocument(doc, this.context); + context.setDoc(doc); + } + + 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 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; + } + + /** + * 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 AbstractSyndEntrySource.CONTENT_TYPE} parameter is used correctly. + */ + public void testSourceContentType() + { + Map instanceParams = new HashMap(); + instanceParams.put(AbstractSyndEntrySource.CONTENT_TYPE, SVG_MIME_TYPE); + source.setParams(instanceParams); + assertEquals(PARAMETERS_IGNORED, SVG_MIME_TYPE, source(doc).getDescription().getType()); + + Map methodParams = new HashMap(); + methodParams.put(AbstractSyndEntrySource.CONTENT_TYPE, PNG_MIME_TYPE); + SyndEntry entry = source(doc, methodParams); + assertEquals(PARAMETERS_IGNORED, PNG_MIME_TYPE, entry.getDescription().getType()); + } +} Index: xwiki-platform-core/xwiki-core/src/test/java/com/xpn/xwiki/plugin/feed/SyndEntryArticleSourceTest.java =================================================================== --- xwiki-platform-core/xwiki-core/src/test/java/com/xpn/xwiki/plugin/feed/SyndEntryArticleSourceTest.java (revision 0) +++ xwiki-platform-core/xwiki-core/src/test/java/com/xpn/xwiki/plugin/feed/SyndEntryArticleSourceTest.java (revision 0) @@ -0,0 +1,150 @@ +/* + * 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.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.xpn.xwiki.XWikiContext; +import com.xpn.xwiki.XWikiException; +import com.xpn.xwiki.doc.XWikiDocument; +import com.xpn.xwiki.objects.classes.BaseClass; + +/** + * Unit tests for {@link SyndEntryArticleSource} + */ +public class SyndEntryArticleSourceTest extends AbstractSyndEntrySourceTest +{ + public static final String ARTICLE_CLASS_NAME = "XWiki.ArticleClass"; + + protected void setUp() throws Exception + { + super.setUp(); + + 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); + + source = new SyndEntryArticleSource(); + } + + 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; + } + + /** + * 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 AbstractSyndEntrySource.innerTextLength(AbstractSyndEntrySource.tidy(xmlFragment, + AbstractSyndEntrySource.TIDY_HTML_CONFIG)); + } + + /** + * Tests if {@link AbstractSyndEntrySource#CONTENT_LENGTH} parameter is used correctly when the + * {@link AbstractSyndEntrySource#CONTENT_TYPE} is text/plain. + */ + public void testSourcePlainContentLength() + { + int maxLength = 15; + Map params = new HashMap(); + params.put(AbstractSyndEntrySource.CONTENT_TYPE, "text/plain"); + params.put(AbstractSyndEntrySource.CONTENT_LENGTH, new Integer(maxLength)); + 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 AbstractSyndEntrySource#CONTENT_LENGTH} parameter is used correctly when the + * {@link AbstractSyndEntrySource#CONTENT_TYPE} is text/html. + */ + public void testSourceHTMLContentLength() + { + int maxLength = 16; + Map params = new HashMap(); + params.put(AbstractSyndEntrySource.CONTENT_TYPE, "text/html"); + params.put(AbstractSyndEntrySource.CONTENT_LENGTH, new Integer(maxLength)); + 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 testSourceXMLContentLength() + { + int maxLength = 17; + Map params = new HashMap(); + params.put(AbstractSyndEntrySource.CONTENT_TYPE, "text/xml"); + params.put(AbstractSyndEntrySource.CONTENT_LENGTH, new Integer(maxLength)); + 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,115 @@ +/* + * 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.ArrayList; +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.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; + +/** + * Concrete strategy for computing the field values of a feed entry from any {@link XWikiDocument} + * instance. + */ +public class SyndEntryDocumentSource extends AbstractSyndEntrySource +{ + /** + * @see AbstractSyndEntrySource#getDefaultParams() + */ + public static final Map defaultParams; + + static { + defaultParams = new HashMap(); + defaultParams.put(CONTENT_TYPE, "text/html"); + defaultParams.put(CONTENT_LENGTH, new Integer(250)); + } + + public SyndEntryDocumentSource() + { + this(Collections.EMPTY_MAP); + } + + public SyndEntryDocumentSource(Map params) + { + super(params); + } + + /** + * {@inheritDoc} + * + * @see AbstractSyndEntrySource#getDefaultParams() + */ + protected Map getDefaultParams() + { + return defaultParams; + } + + /** + * {@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()); + String contentType = (String) trueParams.get(CONTENT_TYPE); + + // compute field values + XWiki xwiki = context.getWiki(); + + String url = doc.getExternalURL("view", "language=" + doc.getRealLanguage()); + String creator = xwiki.getUserName(doc.getCreator(), null, false, context); + 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"; + String description = + String.format(descFormat, new Object[] {doc.getVersion(), author, doc.getDate()}); + List contributors = new ArrayList(); + contributors.add(author); + + // fill the feed entry with computed field values + entry.setUri(url); + entry.setLink(url); + entry.setTitle(doc.getDisplayTitle()); + entry.setDescription(getSyndContent(contentType, description)); + entry.setPublishedDate(doc.getCreationDate()); + entry.setUpdatedDate(doc.getDate()); + entry.setAuthor(creator); + entry.setContributors(contributors); + } +} 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 9235) +++ 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/SyndEntryArticleSource.java =================================================================== --- xwiki-platform-core/xwiki-core/src/main/java/com/xpn/xwiki/plugin/feed/SyndEntryArticleSource.java (revision 0) +++ xwiki-platform-core/xwiki-core/src/main/java/com/xpn/xwiki/plugin/feed/SyndEntryArticleSource.java (revision 0) @@ -0,0 +1,123 @@ +/* + * 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.ArrayList; +import java.util.Arrays; +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.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; + +/** + * Concrete strategy for computing the field values of a feed entry from a {@link XWikiDocument} + * instance containing an XWiki.ArticleClass object. + */ +public class SyndEntryArticleSource extends AbstractSyndEntrySource +{ + /** + * @see AbstractSyndEntrySource#getDefaultParams() + */ + public static final Map defaultParams; + + static { + defaultParams = new HashMap(); + defaultParams.put(CONTENT_TYPE, "text/html"); + defaultParams.put(CONTENT_LENGTH, new Integer(400)); + } + + public SyndEntryArticleSource() + { + this(Collections.EMPTY_MAP); + } + + public SyndEntryArticleSource(Map params) + { + super(params); + } + + /** + * {@inheritDoc} + * + * @see AbstractSyndEntrySource#getDefaultParams() + */ + protected Map getDefaultParams() + { + return defaultParams; + } + + /** + * {@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()); + String contentType = (String) trueParams.get(CONTENT_TYPE); + int contentLength = ((Number) trueParams.get(CONTENT_LENGTH)).intValue(); + + // compute field values + XWiki xwiki = context.getWiki(); + doc.use("XWiki.ArticleClass"); + + String url = doc.getExternalURL("view", "language=" + doc.getRealLanguage()); + String description = context.getDoc().getRenderedContent(doc.display("content"), context); + 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); + } + List categories = Arrays.asList(doc.display("category").split(",")); + String creator = xwiki.getUserName(doc.getCreator(), null, false, context); + List contributors = new ArrayList(); + contributors.add(xwiki.getUserName(doc.getAuthor(), null, false, context)); + + // fill the feed entry with computed field values + entry.setUri(url); + entry.setLink(url); + entry.setTitle(doc.display("title")); + entry.setDescription(getSyndContent(contentType, description)); + entry.setCategories(categories); + entry.setPublishedDate(doc.getCreationDate()); + entry.setUpdatedDate(doc.getDate()); + entry.setAuthor(creator); + entry.setContributors(contributors); + } +} Index: xwiki-platform-core/xwiki-core/src/main/java/com/xpn/xwiki/plugin/feed/AbstractSyndEntrySource.java =================================================================== --- xwiki-platform-core/xwiki-core/src/main/java/com/xpn/xwiki/plugin/feed/AbstractSyndEntrySource.java (revision 0) +++ xwiki-platform-core/xwiki-core/src/main/java/com/xpn/xwiki/plugin/feed/AbstractSyndEntrySource.java (revision 0) @@ -0,0 +1,308 @@ +/* + * 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.Collections; +import java.util.HashMap; +import java.util.Iterator; +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.XWikiContext; +import com.xpn.xwiki.XWikiException; +import com.xpn.xwiki.api.Document; +import com.xpn.xwiki.doc.XWikiDocument; +import com.xpn.xwiki.util.ExtractHandler; + +/** + * Abstract strategy for computing the field values of a feed entry + */ +public abstract class AbstractSyndEntrySource implements SyndEntrySource +{ + 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; + + static { + 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"); + + 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"); + + 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"); + } + + /** + * 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 AbstractSyndEntrySource(Map params) + { + setParams(params); + } + + public Map getParams() + { + return params; + } + + public void setParams(Map params) + { + this.params = joinParams(params, getDefaultParams()); + } + + /** + * Strategy class parameters + */ + protected Map getDefaultParams() + { + return Collections.EMPTY_MAP; + } + + /** + * @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/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 9235) +++ xwiki-platform-core/xwiki-core/src/main/java/com/xpn/xwiki/plugin/feed/FeedPluginApi.java (working copy) @@ -23,13 +23,24 @@ import java.io.IOException; import java.util.Collection; +import java.util.Collections; +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 +179,685 @@ 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. + * + * @return a new {@link SyndEntrySourceApi} wrapping a {@link SyndEntryDocumentSource} object + */ + public SyndEntrySourceApi getSyndEntryDocumentSource() + { + return this.getSyndEntrySource(SyndEntryDocumentSource.class.getName()); + } + + /** + * Instantiates the default strategy for converting articles in feed entries. + * + * @return a new {@link SyndEntrySourceApi} wrapping a {@link SyndEntryArticleSource} object + */ + public SyndEntrySourceApi getSyndEntryArticleSource() + { + return this.getSyndEntrySource(SyndEntryArticleSource.class.getName()); + } + + /** + * 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. + * + * @param list a list of {@link Document} objects, {@link XWikiDocument} objects or document + * names + * @return a new feed + * @see Document + * @see #getFeed(List, SyndEntrySourceApi, Map) + * @see SyndEntryDocumentSource + */ + public SyndFeed getDocumentFeed(List list) + { + return getDocumentFeed(list, 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. + * + * @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 Document + * @see #getFeed(String, int, int, SyndEntrySourceApi, Map) + * @see SyndEntryDocumentSource + */ + public SyndFeed getDocumentFeed(String query, int count, int start) + { + return getDocumentFeed(query, count, start, 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. + * + * @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 Document + * @see #getFeed(List, SyndEntrySourceApi, Map, Map) + * @see SyndEntryDocumentSource + */ + public SyndFeed getDocumentFeed(List list, Map metadata) + { + return getFeed(list, getSyndEntryDocumentSource(), Collections.EMPTY_MAP, 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, 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. + * + * @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 Document + * @see #getFeed(String, int, int, SyndEntrySourceApi, Map, Map) + * @see SyndEntryDocumentSource + */ + public SyndFeed getDocumentFeed(String query, int count, int start, Map metadata) + { + return getFeed(query, count, start, getSyndEntryDocumentSource(), Collections.EMPTY_MAP, + 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, 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 metadata, String type) + { + return getFeedOutput(list, getSyndEntryDocumentSource(), Collections.EMPTY_MAP, 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 metadata, + String type) + { + return getFeedOutput(query, count, start, getSyndEntryDocumentSource(), + Collections.EMPTY_MAP, 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,245 @@ +/* + * 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(""); + } + + 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 = 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; + } +}