/* Copyright (2006-2012) Schibsted ASA
* This file is part of Possom.
*
* Possom 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.
*
* Possom 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 Possom. If not, see <http://www.gnu.org/licenses/>.
* SyndicationGenerator.java
*
* Created on June 7, 2006, 2:39 PM
*/
package no.sesat.search.view.output;
import com.sun.syndication.feed.synd.SyndContent;
import com.sun.syndication.feed.synd.SyndContentImpl;
import com.sun.syndication.feed.synd.SyndEnclosure;
import com.sun.syndication.feed.synd.SyndEnclosureImpl;
import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.feed.synd.SyndEntryImpl;
import com.sun.syndication.feed.synd.SyndFeed;
import com.sun.syndication.feed.synd.SyndFeedImpl;
import com.sun.syndication.io.FeedException;
import com.sun.syndication.io.SyndFeedOutput;
import no.sesat.search.InfrastructureException;
import no.sesat.search.datamodel.DataModelContext;
import no.sesat.search.datamodel.generic.StringDataObject;
import no.sesat.search.result.ResultItem;
import no.sesat.search.result.ResultList;
import no.sesat.search.site.Site;
import no.sesat.search.site.SiteContext;
import no.sesat.search.site.config.PropertiesLoader;
import no.sesat.search.site.config.ResourceContext;
import no.sesat.search.view.config.SearchTab;
import no.sesat.search.site.config.TextMessages;
import no.sesat.search.view.output.syndication.modules.SearchResultModule;
import no.sesat.search.view.output.syndication.modules.SearchResultModuleImpl;
import no.sesat.search.view.velocity.VelocityEngineFactory;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.log4j.Logger;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.exception.MethodInvocationException;
import org.apache.velocity.exception.ParseErrorException;
import org.apache.velocity.exception.ResourceNotFoundException;
import java.io.StringWriter;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import java.util.TimeZone;
import no.sesat.search.result.BasicResultList;
/**
* Used by the rssDecorator.jsp to print out the results in rss format.
*
*
*/
public final class SyndicationGenerator {
/**
* The context this class needs to do its job.
*/
public interface Context extends SiteContext, DataModelContext, ResourceContext {
/**
* The tab to generate rss for.
*
* @return The search tab to generate rss for.
*/
SearchTab getTab();
/**
* The complete URL of the original page the rss represents.
*
* @return the url of the original page.
*/
String getURL();
}
// Constants -----------------------------------------------------
// Any other way to get rid of the dc:date tags that ROME generates.
private static final Logger LOG = Logger.getLogger(SyndicationGenerator.class);
private static final String DCDATE_PATTERN = "<dc:date>[^<]+</dc:date>";
private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
private static final String ERR_TEMPLATE_NOT_FOUND = " Unable to find template for rss field: ";
private static final String ERR_TEMPLATE_ERR = " Parse error in template: ";
private static final String DEBUG_USING_DEFAULT_DATE_FORMAT = "Using default date format";
// Attributes ----------------------------------------------------
private final Context context;
private final ResultList<ResultItem> result;
private final Site site;
private final TextMessages text;
private String feedType = "rss_2.0";
private final String templateDir;
private final VelocityEngine engine;
private final String uri;
//private final Channels channels;
private String encoding = "UTF-8";
private String nowStringUTC;
// Static --------------------------------------------------------
// Constructors --------------------------------------------------
/**
* Creates a new instance.
*
* @param context The context this class needs to do its work.
* @throws SyndicationNotSupportedException
*/
public SyndicationGenerator(final Context context) throws SyndicationNotSupportedException{
if(null == context.getTab().getRssResultName()){ throw new SyndicationNotSupportedException(); }
this.context = context;
this.result = null != context.getDataModel().getSearch(context.getTab().getRssResultName())
? context.getDataModel().getSearch(context.getTab().getRssResultName()).getResults()
: new BasicResultList<ResultItem>();
this.site = context.getSite();
this.text = TextMessages.valueOf(getTextMessagesContext());
this.uri = context.getURL();
final String type = getParameter("feedType");
if (! "".equals(type)) {
this.feedType = type;
}
final String enc = getParameter("encoding");
if (! "".equals(enc)) {
if (encoding.equalsIgnoreCase("iso-8859-1")) {
this.encoding = "iso-8859-1";
}
}
templateDir = "rss/" + context.getTab().getId() + "/";
engine = VelocityEngineFactory.valueOf(site).getEngine();
}
// Public --------------------------------------------------------
/**
* Returns the generated rss content.
*
* @return the rss document.
*/
public String generate() {
String dfString = DEFAULT_DATE_FORMAT;
try {
dfString = render("dateFormat_publishedDate", null, 0);
} catch (ResourceNotFoundException ex) {
LOG.trace(DEBUG_USING_DEFAULT_DATE_FORMAT);
}
final DateFormat df = new SimpleDateFormat(dfString);
// Zulu time is UTC. But java doesn't know that.
if (dfString.endsWith("'Z'")) {
df.setTimeZone(TimeZone.getTimeZone("UTC"));
}
nowStringUTC = df.format(new Date());
try {
final SyndFeed feed = new SyndFeedImpl();
final SearchResultModule m = new SearchResultModuleImpl();
m.setNumberOfHits(Integer.toString(result.getHitCount()));
final List<SearchResultModule> modules = new ArrayList<SearchResultModule>();
modules.add(m);
feed.setModules(modules);
feed.setEncoding(this.encoding);
feed.setFeedType(feedType);
feed.setDescription(StringEscapeUtils.unescapeXml(render("description", null, 0)));
feed.setTitle(StringEscapeUtils.unescapeXml(render("title", null, 0)));
feed.setPublishedDate(new Date());
feed.setLink(render("link", null, 0));
final List<SyndEntry> entries = new ArrayList<SyndEntry>();
int idx = 0;
for (ResultItem item : result.getResults()) {
++idx;
final SyndEntry entry = new SyndEntryImpl();
final SearchResultModule entryModule = new SearchResultModuleImpl();
if (item.getField("age") != null && !"".equals(item.getField("age"))) {
entryModule.setArticleAge(item.getField("age"));
}
if (item.getField("newssource") != null && !"".equals(item.getField("newssource"))) {
entryModule.setNewsSource(item.getField("newssource"));
}
final List<SearchResultModule> sModules = new ArrayList<SearchResultModule>();
sModules.add(entryModule);
entry.setModules(sModules);
final SyndContent content = new SyndContentImpl();
content.setType("text/html");
final String entryDescription = render("entryDescription", item, idx);
content.setValue(StringEscapeUtils.unescapeHtml(entryDescription));
final String publishedDate = render("entryPublishedDate", item, idx);
try {
final Date date = df.parse(publishedDate);
if (date.getTime() > 0) {
entry.setPublishedDate(df.parse(publishedDate));
} else {
LOG.debug("Publish date set to epoch. Ignoring");
}
} catch (ParseException ex) {
if (!(publishedDate == null || publishedDate.trim().equals(""))) {
LOG.error("Cannot parse " + publishedDate + " using df " + dfString);
} else {
LOG.debug("Publish date is empty. Using current time");
}
entry.setPublishedDate(new Date());
}
entry.setTitle(render("entryTitle", item, idx));
entry.setLink(render("entryUri", item, idx));
try {
final SyndEnclosure enclosure = new SyndEnclosureImpl();
enclosure.setUrl(render("entryEnclosure", item, idx));
final List<SyndEnclosure> enclosures = new ArrayList<SyndEnclosure>();
enclosures.add(enclosure);
entry.setEnclosures(enclosures);
// @todo. specific to sesam.no. put somewhere else...
if ("swip".equals(context.getTab().getKey())) {
enclosure.setType("image/gif");
} else {
enclosure.setType("image/png");
}
} catch (ResourceNotFoundException ex) {
LOG.debug("Template for enclosure not found. Skipping.");
}
final List<SyndContent> contents = new ArrayList<SyndContent>();
contents.add(content);
entry.setContents(contents);
entry.setDescription(content);
entries.add(entry);
}
feed.setEntries(entries);
final SyndFeedOutput output = new SyndFeedOutput();
return output.outputString(feed).replaceAll(DCDATE_PATTERN, "");
} catch (ResourceNotFoundException ex) {
throw new RuntimeException(ex);
} catch (FeedException ex) {
throw new RuntimeException(ex);
}
}
// Package protected ---------------------------------------------
// Protected -----------------------------------------------------
// Private -------------------------------------------------------
private String render(
final String name,
final ResultItem item,
final int itemIdx) throws ResourceNotFoundException {
final String templateUri = templateDir + name;
try {
final VelocityContext cxt = VelocityEngineFactory.newContextInstance();
cxt.put("text", text);
cxt.put("now", nowStringUTC);
if (item != null) {
cxt.put("item", item);
cxt.put("itemIdx", itemIdx);
}
cxt.put("datamodel", context.getDataModel());
final String origUri = uri.replaceAll("&?layout=[^&]+", "").replaceAll("&?feedtype=[^&]+", "");
cxt.put("uri", origUri);
final Template tpl = VelocityEngineFactory.getTemplate(engine, site, templateUri);
final StringWriter writer = new StringWriter();
tpl.merge(cxt, writer);
return writer.toString();
} catch (ParseErrorException ex) {
LOG.error(ERR_TEMPLATE_ERR + templateUri);
throw new InfrastructureException(ex);
} catch (MethodInvocationException ex) {
throw new InfrastructureException(ex);
} catch (ResourceNotFoundException ex) {
LOG.debug(ERR_TEMPLATE_NOT_FOUND + templateUri);
throw ex;
} catch (Exception ex) {
throw new InfrastructureException(ex);
}
}
private String getParameter(final String parameterName) {
final StringDataObject value = context.getDataModel().getParameters().getValue(parameterName);
if (value != null) {
return value.getUtf8UrlEncoded();
} else {
return "";
}
}
private TextMessages.Context getTextMessagesContext() {
return new TextMessages.Context() {
public Site getSite() {
return context.getSite();
}
public PropertiesLoader newPropertiesLoader(
final SiteContext siteCxt,
final String resource,
final Properties properties) {
return context.newPropertiesLoader(siteCxt, resource, properties);
}
};
}
// private Channels.Context getChannelContext() {
// return new Channels.Context() {
// public Site getSite() {
// return context.getSite();
// }
// public DocumentLoader newDocumentLoader(
// final SiteContext cxt,
// final String resource,
// final DocumentBuilder builder) {
// return context.newDocumentLoader(cxt, resource, builder);
// }
// };
// }
// Inner classes -------------------------------------------------
public static final class SyndicationNotSupportedException extends Exception{
}
}