| 1 | |
|
| 2 | |
|
| 3 | |
|
| 4 | |
|
| 5 | |
|
| 6 | |
|
| 7 | |
|
| 8 | |
|
| 9 | |
|
| 10 | |
|
| 11 | |
|
| 12 | |
|
| 13 | |
|
| 14 | |
|
| 15 | |
|
| 16 | |
|
| 17 | |
|
| 18 | |
|
| 19 | |
|
| 20 | |
|
| 21 | |
|
| 22 | |
package no.sesat.search.view.output; |
| 23 | |
|
| 24 | |
import com.sun.syndication.feed.synd.SyndContent; |
| 25 | |
import com.sun.syndication.feed.synd.SyndContentImpl; |
| 26 | |
import com.sun.syndication.feed.synd.SyndEnclosure; |
| 27 | |
import com.sun.syndication.feed.synd.SyndEnclosureImpl; |
| 28 | |
import com.sun.syndication.feed.synd.SyndEntry; |
| 29 | |
import com.sun.syndication.feed.synd.SyndEntryImpl; |
| 30 | |
import com.sun.syndication.feed.synd.SyndFeed; |
| 31 | |
import com.sun.syndication.feed.synd.SyndFeedImpl; |
| 32 | |
import com.sun.syndication.io.FeedException; |
| 33 | |
import com.sun.syndication.io.SyndFeedOutput; |
| 34 | |
import no.sesat.search.InfrastructureException; |
| 35 | |
import no.sesat.search.datamodel.DataModelContext; |
| 36 | |
import no.sesat.search.datamodel.generic.StringDataObject; |
| 37 | |
import no.sesat.search.result.ResultItem; |
| 38 | |
import no.sesat.search.result.ResultList; |
| 39 | |
import no.sesat.search.site.Site; |
| 40 | |
import no.sesat.search.site.SiteContext; |
| 41 | |
import no.sesat.search.site.config.PropertiesLoader; |
| 42 | |
import no.sesat.search.site.config.ResourceContext; |
| 43 | |
import no.sesat.search.view.config.SearchTab; |
| 44 | |
import no.sesat.search.site.config.TextMessages; |
| 45 | |
import no.sesat.search.view.output.syndication.modules.SearchResultModule; |
| 46 | |
import no.sesat.search.view.output.syndication.modules.SearchResultModuleImpl; |
| 47 | |
import no.sesat.search.view.velocity.VelocityEngineFactory; |
| 48 | |
import org.apache.commons.lang.StringEscapeUtils; |
| 49 | |
import org.apache.log4j.Logger; |
| 50 | |
import org.apache.velocity.Template; |
| 51 | |
import org.apache.velocity.VelocityContext; |
| 52 | |
import org.apache.velocity.app.VelocityEngine; |
| 53 | |
import org.apache.velocity.exception.MethodInvocationException; |
| 54 | |
import org.apache.velocity.exception.ParseErrorException; |
| 55 | |
import org.apache.velocity.exception.ResourceNotFoundException; |
| 56 | |
|
| 57 | |
import java.io.StringWriter; |
| 58 | |
import java.text.DateFormat; |
| 59 | |
import java.text.ParseException; |
| 60 | |
import java.text.SimpleDateFormat; |
| 61 | |
import java.util.ArrayList; |
| 62 | |
import java.util.Date; |
| 63 | |
import java.util.List; |
| 64 | |
import java.util.Properties; |
| 65 | |
import java.util.TimeZone; |
| 66 | |
import javax.resource.NotSupportedException; |
| 67 | |
import no.sesat.search.result.BasicResultList; |
| 68 | |
|
| 69 | |
|
| 70 | |
|
| 71 | |
|
| 72 | |
|
| 73 | |
|
| 74 | 0 | public final class SyndicationGenerator { |
| 75 | |
|
| 76 | |
|
| 77 | |
|
| 78 | |
|
| 79 | |
public interface Context extends SiteContext, DataModelContext, ResourceContext { |
| 80 | |
|
| 81 | |
|
| 82 | |
|
| 83 | |
|
| 84 | |
|
| 85 | |
SearchTab getTab(); |
| 86 | |
|
| 87 | |
|
| 88 | |
|
| 89 | |
|
| 90 | |
|
| 91 | |
|
| 92 | |
String getURL(); |
| 93 | |
} |
| 94 | |
|
| 95 | |
|
| 96 | |
|
| 97 | |
|
| 98 | 0 | private static final Logger LOG = Logger.getLogger(SyndicationGenerator.class); |
| 99 | |
|
| 100 | |
private static final String DCDATE_PATTERN = "<dc:date>[^<]+</dc:date>"; |
| 101 | |
|
| 102 | |
private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; |
| 103 | |
private static final String ERR_TEMPLATE_NOT_FOUND = " Unable to find template for rss field: "; |
| 104 | |
private static final String ERR_TEMPLATE_ERR = " Parse error in template: "; |
| 105 | |
private static final String DEBUG_USING_DEFAULT_DATE_FORMAT = "Using default date format"; |
| 106 | |
|
| 107 | |
|
| 108 | |
|
| 109 | |
private final Context context; |
| 110 | |
|
| 111 | |
private final ResultList<ResultItem> result; |
| 112 | |
private final Site site; |
| 113 | |
private final TextMessages text; |
| 114 | 0 | private String feedType = "rss_2.0"; |
| 115 | |
private final String templateDir; |
| 116 | |
private final VelocityEngine engine; |
| 117 | |
private final String uri; |
| 118 | |
|
| 119 | 0 | private String encoding = "UTF-8"; |
| 120 | |
private String nowStringUTC; |
| 121 | |
|
| 122 | |
|
| 123 | |
|
| 124 | |
|
| 125 | |
|
| 126 | |
|
| 127 | |
|
| 128 | |
|
| 129 | |
|
| 130 | |
|
| 131 | |
|
| 132 | 0 | public SyndicationGenerator(final Context context) throws SyndicationNotSupportedException{ |
| 133 | |
|
| 134 | 0 | if(null == context.getTab().getRssResultName()){ throw new SyndicationNotSupportedException(); } |
| 135 | |
|
| 136 | 0 | this.context = context; |
| 137 | |
|
| 138 | 0 | this.result = null != context.getDataModel().getSearch(context.getTab().getRssResultName()) |
| 139 | |
? context.getDataModel().getSearch(context.getTab().getRssResultName()).getResults() |
| 140 | |
: new BasicResultList<ResultItem>(); |
| 141 | |
|
| 142 | 0 | this.site = context.getSite(); |
| 143 | |
|
| 144 | 0 | this.text = TextMessages.valueOf(getTextMessagesContext()); |
| 145 | 0 | this.uri = context.getURL(); |
| 146 | |
|
| 147 | 0 | final String type = getParameter("feedType"); |
| 148 | |
|
| 149 | 0 | if (! "".equals(type)) { |
| 150 | 0 | this.feedType = type; |
| 151 | |
} |
| 152 | |
|
| 153 | 0 | final String enc = getParameter("encoding"); |
| 154 | 0 | if (! "".equals(enc)) { |
| 155 | 0 | if (encoding.equalsIgnoreCase("iso-8859-1")) { |
| 156 | 0 | this.encoding = "iso-8859-1"; |
| 157 | |
} |
| 158 | |
} |
| 159 | |
|
| 160 | 0 | templateDir = "rss/" + context.getTab().getId() + "/"; |
| 161 | |
|
| 162 | 0 | engine = VelocityEngineFactory.valueOf(site).getEngine(); |
| 163 | 0 | } |
| 164 | |
|
| 165 | |
|
| 166 | |
|
| 167 | |
|
| 168 | |
|
| 169 | |
|
| 170 | |
|
| 171 | |
|
| 172 | |
|
| 173 | |
public String generate() { |
| 174 | |
|
| 175 | 0 | String dfString = DEFAULT_DATE_FORMAT; |
| 176 | |
|
| 177 | |
try { |
| 178 | 0 | dfString = render("dateFormat_publishedDate", null, 0); |
| 179 | 0 | } catch (ResourceNotFoundException ex) { |
| 180 | 0 | LOG.trace(DEBUG_USING_DEFAULT_DATE_FORMAT); |
| 181 | 0 | } |
| 182 | |
|
| 183 | 0 | final DateFormat df = new SimpleDateFormat(dfString); |
| 184 | |
|
| 185 | |
|
| 186 | 0 | if (dfString.endsWith("'Z'")) { |
| 187 | 0 | df.setTimeZone(TimeZone.getTimeZone("UTC")); |
| 188 | |
} |
| 189 | |
|
| 190 | 0 | nowStringUTC = df.format(new Date()); |
| 191 | |
|
| 192 | |
try { |
| 193 | 0 | final SyndFeed feed = new SyndFeedImpl(); |
| 194 | 0 | final SearchResultModule m = new SearchResultModuleImpl(); |
| 195 | |
|
| 196 | 0 | m.setNumberOfHits(Integer.toString(result.getHitCount())); |
| 197 | |
|
| 198 | 0 | final List<SearchResultModule> modules = new ArrayList<SearchResultModule>(); |
| 199 | |
|
| 200 | 0 | modules.add(m); |
| 201 | |
|
| 202 | 0 | feed.setModules(modules); |
| 203 | |
|
| 204 | 0 | feed.setEncoding(this.encoding); |
| 205 | 0 | feed.setFeedType(feedType); |
| 206 | 0 | feed.setDescription(StringEscapeUtils.unescapeXml(render("description", null, 0))); |
| 207 | 0 | feed.setTitle(StringEscapeUtils.unescapeXml(render("title", null, 0))); |
| 208 | 0 | feed.setPublishedDate(new Date()); |
| 209 | 0 | feed.setLink(render("link", null, 0)); |
| 210 | |
|
| 211 | 0 | final List<SyndEntry> entries = new ArrayList<SyndEntry>(); |
| 212 | |
|
| 213 | 0 | int idx = 0; |
| 214 | 0 | for (ResultItem item : result.getResults()) { |
| 215 | 0 | ++idx; |
| 216 | |
|
| 217 | 0 | final SyndEntry entry = new SyndEntryImpl(); |
| 218 | |
|
| 219 | 0 | final SearchResultModule entryModule = new SearchResultModuleImpl(); |
| 220 | |
|
| 221 | 0 | if (item.getField("age") != null && !"".equals(item.getField("age"))) { |
| 222 | 0 | entryModule.setArticleAge(item.getField("age")); |
| 223 | |
} |
| 224 | |
|
| 225 | 0 | if (item.getField("newssource") != null && !"".equals(item.getField("newssource"))) { |
| 226 | 0 | entryModule.setNewsSource(item.getField("newssource")); |
| 227 | |
} |
| 228 | |
|
| 229 | 0 | final List<SearchResultModule> sModules = new ArrayList<SearchResultModule>(); |
| 230 | 0 | sModules.add(entryModule); |
| 231 | 0 | entry.setModules(sModules); |
| 232 | 0 | final SyndContent content = new SyndContentImpl(); |
| 233 | |
|
| 234 | 0 | content.setType("text/html"); |
| 235 | 0 | final String entryDescription = render("entryDescription", item, idx); |
| 236 | |
|
| 237 | 0 | content.setValue(StringEscapeUtils.unescapeHtml(entryDescription)); |
| 238 | |
|
| 239 | 0 | final String publishedDate = render("entryPublishedDate", item, idx); |
| 240 | |
|
| 241 | |
try { |
| 242 | 0 | final Date date = df.parse(publishedDate); |
| 243 | |
|
| 244 | 0 | if (date.getTime() > 0) { |
| 245 | 0 | entry.setPublishedDate(df.parse(publishedDate)); |
| 246 | |
} else { |
| 247 | 0 | LOG.debug("Publish date set to epoch. Ignoring"); |
| 248 | |
} |
| 249 | 0 | } catch (ParseException ex) { |
| 250 | 0 | if (!(publishedDate == null || publishedDate.trim().equals(""))) { |
| 251 | 0 | LOG.error("Cannot parse " + publishedDate + " using df " + dfString); |
| 252 | |
} else { |
| 253 | 0 | LOG.debug("Publish date is empty. Using current time"); |
| 254 | |
} |
| 255 | |
|
| 256 | 0 | entry.setPublishedDate(new Date()); |
| 257 | 0 | } |
| 258 | |
|
| 259 | 0 | entry.setTitle(render("entryTitle", item, idx)); |
| 260 | 0 | entry.setLink(render("entryUri", item, idx)); |
| 261 | |
|
| 262 | |
|
| 263 | |
try { |
| 264 | 0 | final SyndEnclosure enclosure = new SyndEnclosureImpl(); |
| 265 | |
|
| 266 | 0 | enclosure.setUrl(render("entryEnclosure", item, idx)); |
| 267 | |
|
| 268 | 0 | final List<SyndEnclosure> enclosures = new ArrayList<SyndEnclosure>(); |
| 269 | 0 | enclosures.add(enclosure); |
| 270 | 0 | entry.setEnclosures(enclosures); |
| 271 | |
|
| 272 | |
|
| 273 | 0 | if ("swip".equals(context.getTab().getKey())) { |
| 274 | 0 | enclosure.setType("image/gif"); |
| 275 | |
} else { |
| 276 | 0 | enclosure.setType("image/png"); |
| 277 | |
} |
| 278 | 0 | } catch (ResourceNotFoundException ex) { |
| 279 | 0 | LOG.debug("Template for enclosure not found. Skipping."); |
| 280 | 0 | } |
| 281 | |
|
| 282 | |
|
| 283 | 0 | final List<SyndContent> contents = new ArrayList<SyndContent>(); |
| 284 | |
|
| 285 | 0 | contents.add(content); |
| 286 | |
|
| 287 | 0 | entry.setContents(contents); |
| 288 | 0 | entry.setDescription(content); |
| 289 | |
|
| 290 | 0 | entries.add(entry); |
| 291 | 0 | } |
| 292 | |
|
| 293 | 0 | feed.setEntries(entries); |
| 294 | |
|
| 295 | 0 | final SyndFeedOutput output = new SyndFeedOutput(); |
| 296 | 0 | return output.outputString(feed).replaceAll(DCDATE_PATTERN, ""); |
| 297 | 0 | } catch (ResourceNotFoundException ex) { |
| 298 | 0 | throw new RuntimeException(ex); |
| 299 | 0 | } catch (FeedException ex) { |
| 300 | 0 | throw new RuntimeException(ex); |
| 301 | |
} |
| 302 | |
} |
| 303 | |
|
| 304 | |
|
| 305 | |
|
| 306 | |
|
| 307 | |
|
| 308 | |
|
| 309 | |
|
| 310 | |
private String render( |
| 311 | |
final String name, |
| 312 | |
final ResultItem item, |
| 313 | |
final int itemIdx) throws ResourceNotFoundException { |
| 314 | |
|
| 315 | 0 | final String templateUri = templateDir + name; |
| 316 | |
|
| 317 | |
try { |
| 318 | 0 | final VelocityContext cxt = VelocityEngineFactory.newContextInstance(); |
| 319 | |
|
| 320 | 0 | cxt.put("text", text); |
| 321 | 0 | cxt.put("now", nowStringUTC); |
| 322 | |
|
| 323 | 0 | if (item != null) { |
| 324 | 0 | cxt.put("item", item); |
| 325 | 0 | cxt.put("itemIdx", itemIdx); |
| 326 | |
} |
| 327 | |
|
| 328 | 0 | cxt.put("datamodel", context.getDataModel()); |
| 329 | |
|
| 330 | 0 | final String origUri = uri.replaceAll("&?layout=[^&]+", "").replaceAll("&?feedtype=[^&]+", ""); |
| 331 | 0 | cxt.put("uri", origUri); |
| 332 | |
|
| 333 | 0 | final Template tpl = VelocityEngineFactory.getTemplate(engine, site, templateUri); |
| 334 | |
|
| 335 | 0 | final StringWriter writer = new StringWriter(); |
| 336 | 0 | tpl.merge(cxt, writer); |
| 337 | |
|
| 338 | 0 | return writer.toString(); |
| 339 | |
|
| 340 | 0 | } catch (ParseErrorException ex) { |
| 341 | 0 | LOG.error(ERR_TEMPLATE_ERR + templateUri); |
| 342 | 0 | throw new InfrastructureException(ex); |
| 343 | |
|
| 344 | 0 | } catch (MethodInvocationException ex) { |
| 345 | 0 | throw new InfrastructureException(ex); |
| 346 | |
|
| 347 | 0 | } catch (ResourceNotFoundException ex) { |
| 348 | 0 | LOG.debug(ERR_TEMPLATE_NOT_FOUND + templateUri); |
| 349 | 0 | throw ex; |
| 350 | |
|
| 351 | 0 | } catch (Exception ex) { |
| 352 | 0 | throw new InfrastructureException(ex); |
| 353 | |
} |
| 354 | |
} |
| 355 | |
|
| 356 | |
private String getParameter(final String parameterName) { |
| 357 | 0 | final StringDataObject value = context.getDataModel().getParameters().getValue(parameterName); |
| 358 | |
|
| 359 | 0 | if (value != null) { |
| 360 | 0 | return value.toString(); |
| 361 | |
} else { |
| 362 | 0 | return ""; |
| 363 | |
} |
| 364 | |
} |
| 365 | |
|
| 366 | |
private TextMessages.Context getTextMessagesContext() { |
| 367 | 0 | return new TextMessages.Context() { |
| 368 | |
public Site getSite() { |
| 369 | 0 | return context.getSite(); |
| 370 | |
} |
| 371 | |
|
| 372 | |
public PropertiesLoader newPropertiesLoader( |
| 373 | |
final SiteContext siteCxt, |
| 374 | |
final String resource, |
| 375 | |
final Properties properties) { |
| 376 | 0 | return context.newPropertiesLoader(siteCxt, resource, properties); |
| 377 | |
} |
| 378 | |
}; |
| 379 | |
} |
| 380 | |
|
| 381 | |
|
| 382 | |
|
| 383 | |
|
| 384 | |
|
| 385 | |
|
| 386 | |
|
| 387 | |
|
| 388 | |
|
| 389 | |
|
| 390 | |
|
| 391 | |
|
| 392 | |
|
| 393 | |
|
| 394 | |
|
| 395 | |
|
| 396 | |
|
| 397 | 0 | public static final class SyndicationNotSupportedException extends Exception{ |
| 398 | |
|
| 399 | |
} |
| 400 | |
} |