001: /*
002: * (C) Copyright 2000 - 2003 Nabh Information Systems, Inc.
003: *
004: * This program is free software; you can redistribute it and/or
005: * modify it under the terms of the GNU General Public License
006: * as published by the Free Software Foundation; either version 2
007: * of the License, or (at your option) any later version.
008: *
009: * This program is distributed in the hope that it will be useful,
010: * but WITHOUT ANY WARRANTY; without even the implied warranty of
011: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
012: * GNU General Public License for more details.
013: *
014: * You should have received a copy of the GNU General Public License
015: * along with this program; if not, write to the Free Software
016: * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
017: *
018: */
019:
020: package com.nabhinc.portlet.rss;
021:
022: import java.io.File;
023: import java.io.FileInputStream;
024: import java.io.IOException;
025: import java.io.InputStream;
026: import java.io.PrintWriter;
027: import java.util.ResourceBundle;
028:
029: import javax.portlet.ActionRequest;
030: import javax.portlet.ActionResponse;
031: import javax.portlet.PortletConfig;
032: import javax.portlet.PortletException;
033: import javax.portlet.PortletMode;
034: import javax.portlet.PortletPreferences;
035: import javax.portlet.PortletURL;
036: import javax.portlet.ReadOnlyException;
037: import javax.portlet.RenderRequest;
038: import javax.portlet.RenderResponse;
039: import javax.portlet.UnavailableException;
040: import javax.portlet.ValidatorException;
041: import javax.portlet.WindowState;
042: import javax.portlet.WindowStateException;
043: import javax.xml.parsers.DocumentBuilder;
044: import javax.xml.parsers.DocumentBuilderFactory;
045:
046: import org.w3c.dom.Document;
047: import org.w3c.dom.Element;
048: import org.xml.sax.InputSource;
049:
050: import com.nabhinc.portlet.base.BasePortlet;
051: import com.nabhinc.util.XMLUtil;
052:
053: /**
054: *
055: *
056: * @author Padmanabh Dabke
057: * (c) 2003 Nabh Information Systems, Inc. All Rights Reserved.
058: */
059: public class RSSPortlet extends BasePortlet {
060:
061: public static final String RSS_DOC_URL = "docURL";
062: public static final String RSS_DOC_PATH = "docPath";
063: public static final String MAX_ITEMS = "maxItems";
064: public static final String CHANNEL = "channel";
065: public static final String REFRESH_PERIOD = "refreshPeriod";
066: public static final String ITEM = "item";
067: public static final String CHANNEL_TITLE = "title";
068: public static final String RDF_ROOT = "rdf:RDF";
069: //public static final String HEADLINE_INFO = "HeadlineInfo";
070: public static final String HEADLINES = "Headlines";
071:
072: /**
073: * Default refresh period is 24 hours.
074: */
075: private final long DEFAULT_REFRESH_PERIOD = 24 * 60 * 60000;
076:
077: private String rpTitle = "Channel Unavailable";
078: /**
079: * URL of the RSS file.
080: */
081: private String rpURL = null;
082:
083: /**
084: * Path to a local RSS file
085: */
086: private String rpPath = null;
087:
088: /**
089: * Last modification file for RSS file.
090: */
091: private long rpModTime = 0;
092:
093: /**
094: * Cached headlines
095: */
096: private HeadlineInfo[] rpHeadlineInfos = null;
097:
098: /**
099: * Time the RSS URL is to be next refreshed.
100: */
101: private long rpNextCheck = 0;
102:
103: /**
104: * Period for refreshing the content of the RSS url.
105: */
106: private long rpRefresh = DEFAULT_REFRESH_PERIOD;
107:
108: /**
109: * Cache of headlines in minimized mode.
110: */
111: private String rpShortCache = null;
112:
113: /**
114: * Number of headlines cached in rpShortCache
115: */
116: private int rpMaxItems = 5;
117:
118: /**
119: * Cache of all headlines.
120: */
121: private String rpFullCache = null;
122:
123: /**
124: * Default label if more headelines are available.
125: */
126: private String rpMoreLabel = "More";
127:
128: private ResourceBundle rpBundle = null;
129:
130: private static final String MORE_LABEL_KEY = "com.nabhinc.portlet.rss.more_label";
131:
132: /**
133: * Array of RSS headlines
134: *
135: */
136: public static class HeadlineInfo {
137:
138: public static final String TITLE = "title";
139: public static final String DESCRIPTION = "description";
140: public static final String LINK = "link";
141:
142: String title = null;
143: String descr = null;
144: String link = null;
145:
146: /**
147: * HeadlineInfo constructor comment.
148: */
149: public HeadlineInfo() {
150: super ();
151: }
152:
153: public HeadlineInfo(String t, String d, String l) {
154: title = t;
155: descr = d;
156: link = l;
157: }
158:
159: /**
160: * Insert the method's description here.
161: * @return java.lang.String
162: */
163: public String toString() {
164: return title;
165: }
166:
167: }
168:
169: public void init(PortletConfig config) throws PortletException {
170: super .init(config);
171:
172: // Set URL to be processed
173: rpURL = config.getInitParameter(RSS_DOC_URL);
174: if (rpURL == null || "".equals(rpURL)) {
175: rpPath = config.getInitParameter(RSS_DOC_PATH);
176: if (rpPath == null)
177: throw new UnavailableException(
178: "Either docPath or docURL must be specified.",
179: -1);
180: rpPath = getRealPath(config.getPortletContext(), rpPath);
181: }
182:
183: // Set refresh rate
184: String refreshStr = null;
185: try {
186: refreshStr = config.getInitParameter(REFRESH_PERIOD);
187: if (refreshStr == null) {
188: rpRefresh = DEFAULT_REFRESH_PERIOD;
189: } else {
190: rpRefresh = Integer.parseInt(refreshStr) * 1000;
191: }
192: } catch (Exception ex) {
193: error("init", "Failed to parse refresh period: "
194: + refreshStr);
195: throw new PortletException(
196: "Failed to parse refresh period: " + refreshStr);
197: }
198:
199: if (rpPath == null) {
200: updateURLHeadlines(rpMaxItems);
201: } else {
202: updateFileHeadlines(rpMaxItems);
203: }
204:
205: }
206:
207: public void doView(RenderRequest request, RenderResponse response)
208: throws PortletException, IOException {
209:
210: PortletPreferences pref = request.getPreferences();
211: String prefMaxItems = pref.getValue(MAX_ITEMS, null);
212: int numItems = 5;
213: if (prefMaxItems != null)
214: numItems = Integer.parseInt(prefMaxItems);
215:
216: if (rpPath == null) {
217: updateURLHeadlines(numItems);
218: } else {
219: updateFileHeadlines(numItems);
220: }
221:
222: if (request.getWindowState().equals(WindowState.MAXIMIZED)
223: || numItems >= rpHeadlineInfos.length) {
224: response.getWriter().write(rpFullCache);
225: } else {
226: PrintWriter w = response.getWriter();
227:
228: if (numItems == rpMaxItems) {
229: w.write(rpShortCache);
230: } else {
231: writeHeadlines(w, numItems);
232: }
233: try {
234: rpBundle = getPortletConfig().getResourceBundle(
235: request.getLocale());
236: try {
237: rpMoreLabel = rpBundle.getString(MORE_LABEL_KEY);
238: } catch (Exception ex) {
239: }
240: if (rpMoreLabel == null || "".equals(rpMoreLabel))
241: rpMoreLabel = "More...";
242: PortletURL renderUrl = response.createRenderURL();
243: renderUrl.setWindowState(WindowState.MAXIMIZED);
244: w.write("<div align=\"right\"><a href=\"");
245: w.write(renderUrl.toString());
246: w.write("\"><i>" + rpMoreLabel
247: + "</i></a><br/></div><p/></td></tr></table>");
248: } catch (WindowStateException ex) {
249: error("doView",
250: "Maximized window is not allowed or not supported.");
251: }
252: }
253: }
254:
255: public void processAction(ActionRequest aReq, ActionResponse aRes)
256: throws PortletException, IOException {
257: PortletPreferences prefs = aReq.getPreferences();
258: String maxItemsStr = aReq.getParameter(MAX_ITEMS);
259:
260: if (maxItemsStr == null) {
261: maxItemsStr = prefs.getValue(MAX_ITEMS, Integer
262: .toString(rpMaxItems));
263: }
264:
265: if (aReq.getPortletMode().equals(PortletMode.EDIT)) {
266: String errorMsg = null;
267: String warnMsg = null;
268: try {
269: Integer.parseInt(maxItemsStr);
270: prefs.setValue(MAX_ITEMS, maxItemsStr);
271: prefs.store();
272: aRes.setPortletMode(PortletMode.VIEW);
273:
274: } catch (ValidatorException ex) {
275: errorMsg = "Error in storing portlet preference.";
276: } catch (NumberFormatException ex) {
277: errorMsg = "Not a valid number for maximum headlines: "
278: + maxItemsStr + ".";
279: } catch (ReadOnlyException ex) {
280: errorMsg = "Unable to set the value for: " + MAX_ITEMS
281: + " with " + maxItemsStr + ".";
282: } catch (IllegalArgumentException ex) {
283: // warnMsg = "Your preference will not be saved since you are not logged in.";
284: aRes.setPortletMode(PortletMode.VIEW);
285: }
286:
287: if (errorMsg != null) {
288: //error("processAction", errorMsg);
289: aRes.setRenderParameter("error", errorMsg);
290:
291: } else if (warnMsg != null) {
292: aRes.setRenderParameter("warning", errorMsg);
293:
294: }
295: }
296: }
297:
298: private void cacheHeadlines() throws PortletException {
299:
300: int len = rpHeadlineInfos.length;
301:
302: StringBuffer sb = new StringBuffer();
303: createCache(len, sb);
304: sb.append("</td></tr></table>");
305:
306: // Cache the content and revise the next check time
307: rpFullCache = sb.toString();
308:
309: if (len > rpMaxItems) {
310: sb = new StringBuffer();
311: createCache(rpMaxItems, sb);
312: rpShortCache = sb.toString();
313: } else {
314: rpShortCache = rpFullCache;
315: }
316:
317: rpNextCheck = System.currentTimeMillis() + rpRefresh;
318: }
319:
320: private void updateFileHeadlines(int maxItems)
321: throws PortletException {
322:
323: File file = new File(rpPath);
324:
325: if (!file.exists()) {
326: error("init", "Invalid file path: " + rpPath);
327: throw new UnavailableException("Invalid file path : "
328: + rpPath);
329: }
330:
331: if (file.lastModified() != rpModTime) {
332: rpModTime = file.lastModified();
333: FileInputStream fis = null;
334: try {
335: fis = new FileInputStream(file);
336: constructHeadlineInfo(fis, maxItems);
337: } catch (IOException ex) {
338: throw new PortletException(
339: "IO exception in reading RSS file.", ex);
340: } finally {
341: if (fis != null)
342: try {
343: fis.close();
344: } catch (IOException ignore) {
345: }
346: }
347: }
348: }
349:
350: private void updateURLHeadlines(int maxItems)
351: throws PortletException {
352: if (System.currentTimeMillis() > rpNextCheck) {
353: // Try opening the URL input stream
354: InputStream is = null;
355:
356: try {
357: is = new java.net.URL(rpURL).openStream();
358: } catch (Exception ex) {
359: error("doView", "Bad RSS URL " + rpURL + ": "
360: + ex.toString());
361: throw new UnavailableException("Bad RSS URL: " + rpURL,
362: 3600);
363: }
364: try {
365: constructHeadlineInfo(is, maxItems);
366: } finally {
367: if (is != null)
368: try {
369: is.close();
370: } catch (IOException igore) {
371: }
372: }
373: }
374: }
375:
376: private void constructHeadlineInfo(InputStream is, int maxItems)
377: throws PortletException {
378:
379: // Parse RSS/RDF document into headlines
380: try {
381: InputSource source = new InputSource(is);
382: //source.setEncoding("UTF-8");
383: DocumentBuilder parser = DocumentBuilderFactory
384: .newInstance().newDocumentBuilder();
385: Document d = parser.parse(source);
386: Element root = d.getDocumentElement();
387: // Check for the RDF element. If found set that to root
388: Element temp = XMLUtil.getSubElement(root, RDF_ROOT);
389: if (temp != null)
390: root = temp;
391:
392: Element channel = XMLUtil.getSubElement(root, CHANNEL);
393: if (channel != null) {
394: rpTitle = XMLUtil.getSubElementText(channel,
395: CHANNEL_TITLE);
396: }
397:
398: Element[] items = XMLUtil.getSubElements(channel, ITEM);
399: if (items.length == 0) {
400: items = XMLUtil.getSubElements(root, ITEM);
401: }
402:
403: int len = items.length;
404: rpHeadlineInfos = new HeadlineInfo[len];
405:
406: for (int i = 0; i < len; i++) {
407: String descr = XMLUtil.getSubElementTextGlobal(
408: items[i], HeadlineInfo.DESCRIPTION);
409:
410: rpHeadlineInfos[i] = new HeadlineInfo(
411: XMLUtil.getSubElementText(items[i],
412: HeadlineInfo.TITLE), descr, XMLUtil
413: .getSubElementText(items[i],
414: HeadlineInfo.LINK));
415: }
416: } catch (Exception ex3) {
417: error("doView", "Bad RSS document at " + rpURL + ".");
418: throw new UnavailableException("Bad RSS document at "
419: + rpURL, 3600);
420:
421: }
422:
423: cacheHeadlines();
424: }
425:
426: private void writeHeadlines(PrintWriter w, int numItems) {
427: w
428: .write("<table width=\"100%\" cellpadding=\"8\" cellspacing=\"0\"><tr><td class=\"portlet-font\">");
429:
430: for (int i = 0; i < numItems; i++) {
431: if (rpHeadlineInfos[i].link != null) {
432: w.write("<a href=\"");
433: w.write(rpHeadlineInfos[i].link);
434: w.write("\" target=\"_blank\">");
435: }
436: if (rpHeadlineInfos[i].title != null)
437: w.write(rpHeadlineInfos[i].title);
438: if (rpHeadlineInfos[i].link != null) {
439: w.write("</a><br/>");
440: } else {
441: w.write("<br/>");
442: }
443: String desc = rpHeadlineInfos[i].descr;
444: if (desc != null)
445: w.write(desc);
446: w.write("<p/>");
447: }
448: // w.write("</td></tr></table>");
449: }
450:
451: private void createCache(int len, StringBuffer sb) {
452: sb
453: .append("<table width=\"100%\" cellpadding=\"8\" cellspacing=\"0\"><tr><td class=\"portlet-font\">");
454:
455: for (int i = 0; i < len; i++) {
456: if (rpHeadlineInfos[i].link != null) {
457: sb.append("<a href=\"");
458: sb.append(rpHeadlineInfos[i].link);
459: sb.append("\" target=\"_blank\">");
460: }
461: if (rpHeadlineInfos[i].title != null) {
462: sb.append(rpHeadlineInfos[i].title);
463: }
464: if (rpHeadlineInfos[i].link != null) {
465: sb.append("</a><br/>");
466: } else {
467: sb.append("<br/>");
468: }
469: String desc = rpHeadlineInfos[i].descr;
470: if (desc != null)
471: sb.append(desc);
472: sb.append("<p/>");
473: }
474: // sb.append("</td></tr></table>");
475: }
476:
477: }
|