/**
* Copyright © 2014 Instituto Superior Técnico
*
* This file is part of FenixEdu CMS.
*
* FenixEdu CMS 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 3 of the License, or
* (at your option) any later version.
*
* FenixEdu CMS 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 FenixEdu CMS. If not, see <http://www.gnu.org/licenses/>.
*/
package org.fenixedu.cms.rss;
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.joining;
import java.io.IOException;
import java.io.StringWriter;
import java.util.Collection;
import java.util.Locale;
import java.util.regex.Pattern;
import javax.xml.stream.XMLEventFactory;
import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.XMLEvent;
import org.fenixedu.bennu.portal.domain.PortalConfiguration;
import org.fenixedu.cms.domain.Category;
import org.fenixedu.cms.domain.Post;
import org.fenixedu.cms.domain.Site;
import org.fenixedu.commons.i18n.LocalizedString;
import pt.ist.fenixframework.core.Project;
import pt.ist.fenixframework.core.exception.ProjectException;
/**
* Service that generates an RSS Feed from either a {@link Site} (in which case the feed info is that of the site, and all the
* site's posts) or a {@link Category} (in which case the feed info is that of the category, and all the category's posts).
*
* The generated RSS is compliant with the <a href="http://cyber.law.harvard.edu/rss/rss.html">RSS 2.0 specification</a>,
* implementing many of the optional feed elements.
*
* @author João Carvalho (joao.pedro.carvalho@tecnico.ulisboa.pt)
*
*/
public class RSSService {
private static final String CMS_VERSION = getCMSVersion();
/**
* Returns the RSS 2.0 feed for the given {@link Site}, containing all the site's posts as items.
*
* @param site
* The site to generate the feed for.
* @param locale
* The locale in which to generate the feed.
* @return
* The XML of the feed
* @throws XMLStreamException
* If an exception occurs while generating the feed
*/
public static String generateRSSForSite(Site site, Locale locale) throws XMLStreamException {
return generateRSS(site.getRssUrl(), site.getFullUrl(), contentOf(site.getName(), locale),
contentOf(site.getDescription(), locale), locale, site.getPostSet());
}
/**
* Returns the RSS 2.0 feed for the given {@link Category}, containing all the category's posts as items.
*
* @param category
* The category to generate the feed for.
* @param locale
* The locale in which to generate the feed.
* @return
* The XML of the feed
* @throws XMLStreamException
* If an exception occurs while generating the feed
*/
public static String generateRSSForCategory(Category category, Locale locale) throws XMLStreamException {
String title = contentOf(category.getName(), locale) + " · " + contentOf(category.getSite().getName(), locale);
return generateRSS(category.getRssUrl(), category.getAddress(), title, title, locale, category.getPostsSet());
}
private static String generateRSS(String rssUrl, String url, String title, String description, Locale locale,
Collection<Post> posts) throws XMLStreamException {
StringWriter strWriter = new StringWriter();
XMLEventWriter writer = XMLOutputFactory.newInstance().createXMLEventWriter(strWriter);
XMLEventFactory eventFactory = XMLEventFactory.newInstance();
XMLEvent nl = eventFactory.createCharacters("\n");
writer.add(eventFactory.createStartDocument());
writer.add(nl);
writer.add(eventFactory.createStartElement("", "", "rss"));
writer.add(eventFactory.createAttribute("version", "2.0"));
writer.add(eventFactory.createAttribute("xmlns:atom", "http://www.w3.org/2005/Atom"));
writer.add(nl);
writer.add(eventFactory.createStartElement("", "", "channel"));
writer.add(nl);
writer.add(eventFactory.createCharacters("\t"));
writer.add(eventFactory.createStartElement("", "", "atom:link"));
writer.add(eventFactory.createAttribute("rel", "self"));
writer.add(eventFactory.createAttribute("type", "application/rss+xml"));
writer.add(eventFactory.createAttribute("href", rssUrl));
writer.add(eventFactory.createEndElement("", "", "atom:link"));
writer.add(nl);
createNode(writer, eventFactory, "title", title);
createNode(writer, eventFactory, "link", url);
createNode(writer, eventFactory, "description", description);
createNode(writer, eventFactory, "language", locale.toLanguageTag());
createNode(writer, eventFactory, "copyright",
contentOf(PortalConfiguration.getInstance().getApplicationCopyright(), locale));
createNode(writer, eventFactory, "webMaster", PortalConfiguration.getInstance().getSupportEmailAddress() + " ("
+ contentOf(PortalConfiguration.getInstance().getApplicationTitle(), locale) + ")");
createNode(writer, eventFactory, "generator", "FenixEdu CMS " + CMS_VERSION);
createNode(writer, eventFactory, "docs", "http://blogs.law.harvard.edu/tech/rss");
createNode(writer, eventFactory, "ttl", "60");
writer.add(nl);
for (Post post : posts) {
// Is this post public?
if (post.isVisible() && post.getCanViewGroup().isMember(null)) {
writePost(locale, writer, post, eventFactory);
}
}
writer.add(eventFactory.createEndElement("", "", "channel"));
writer.add(nl);
writer.add(eventFactory.createEndElement("", "", "rss"));
writer.add(nl);
writer.add(eventFactory.createEndDocument());
writer.close();
return strWriter.toString();
}
// Pattern to remove invalid XML characters
private static final Pattern sanitizeInputForXml = Pattern
.compile("[^\\u0009\\u000A\\u000D\\u0020-\\uD7FF\\uE000-\\uFFFD\uD800\uDC00-\uDBFF\uDFFF]");
@SuppressWarnings("deprecation")
private static void writePost(Locale locale, XMLEventWriter writer, Post post, XMLEventFactory eventFactory)
throws XMLStreamException {
writer.add(eventFactory.createStartElement("", "", "item"));
writer.add(eventFactory.createCharacters("\n"));
createNode(writer, eventFactory, "title", sanitizeInputForXml.matcher(contentOf(post.getName(), locale)).replaceAll(""));
createNode(writer, eventFactory, "description", sanitizeInputForXml.matcher(contentOf(post.getBody(), locale))
.replaceAll(""));
createNode(writer, eventFactory, "link", post.getAddress());
boolean hasEmail = post.getCreatedBy().getProfile() != null && post.getCreatedBy().getProfile().getEmail() != null;
String authorEmail = hasEmail ? post.getCreatedBy().getProfile().getEmail() : "";
createNode(writer, eventFactory, "author", authorEmail + " (" + post.getCreatedBy().getName() + ")");
createNode(writer, eventFactory, "guid", post.getAddress() + "#" + post.getExternalId());
if (!post.getCategoriesSet().isEmpty()) {
createNode(writer, eventFactory, "category",
post.getCategoriesSet().stream().map(cat -> contentOf(cat.getName(), locale)).collect(joining("/")));
}
createNode(writer, eventFactory, "pubDate", post.getCreationDate()
.toString("EEE, dd MMM yyyy HH:mm:ss Z", Locale.ENGLISH));
writer.add(eventFactory.createCharacters("\n"));
writer.add(eventFactory.createEndElement("", "", "item"));
writer.add(eventFactory.createCharacters("\n"));
}
private static String getCMSVersion() {
try {
Project project = Project.fromName("fenixedu-cms");
return project == null ? "" : "v" + project.getVersion();
} catch (IOException | ProjectException e) {
return "";
}
}
private static void createNode(XMLEventWriter eventWriter, XMLEventFactory eventFactory, String name, String value)
throws XMLStreamException {
eventWriter.add(eventFactory.createCharacters("\t"));
eventWriter.add(eventFactory.createStartElement("", "", name));
eventWriter.add(eventFactory.createCharacters(value == null ? "" : value));
eventWriter.add(eventFactory.createEndElement("", "", name));
eventWriter.add(eventFactory.createCharacters("\n"));
}
private static String contentOf(LocalizedString localizedString, Locale locale) {
return ofNullable(ofNullable(localizedString.getContent(locale)).orElse(localizedString.getContent())).orElse("");
}
}