001: /**********************************************************************************
002: * $URL: https://source.sakaiproject.org/svn/web/tags/sakai_2-4-1/news-impl/impl/src/java/org/sakaiproject/news/impl/BasicNewsService.java $
003: * $Id: BasicNewsService.java 28703 2007-04-11 23:29:24Z ajpoland@iupui.edu $
004: ***********************************************************************************
005: *
006: * Copyright (c) 2003, 2004, 2005, 2006 The Sakai Foundation.
007: *
008: * Licensed under the Educational Community License, Version 1.0 (the "License");
009: * you may not use this file except in compliance with the License.
010: * You may obtain a copy of the License at
011: *
012: * http://www.opensource.org/licenses/ecl1.php
013: *
014: * Unless required by applicable law or agreed to in writing, software
015: * distributed under the License is distributed on an "AS IS" BASIS,
016: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017: * See the License for the specific language governing permissions and
018: * limitations under the License.
019: *
020: **********************************************************************************/package org.sakaiproject.news.impl;
021:
022: import java.util.Collection;
023: import java.util.Iterator;
024: import java.util.List;
025: import java.util.Map;
026: import java.util.Set;
027: import java.util.Stack;
028:
029: import org.apache.commons.codec.binary.Base64;
030: import org.apache.commons.logging.Log;
031: import org.apache.commons.logging.LogFactory;
032: import org.sakaiproject.javax.Filter;
033: import org.sakaiproject.memory.api.Cache;
034: import org.sakaiproject.memory.api.MemoryService;
035: import org.sakaiproject.news.api.NewsChannel;
036: import org.sakaiproject.news.api.NewsConnectionException;
037: import org.sakaiproject.news.api.NewsFormatException;
038: import org.sakaiproject.news.api.NewsService;
039: import org.sakaiproject.site.api.Site;
040: import org.sakaiproject.site.api.SitePage;
041: import org.sakaiproject.site.api.ToolConfiguration;
042: import org.sakaiproject.site.cover.SiteService;
043: import org.sakaiproject.tool.api.Tool;
044: import org.sakaiproject.tool.api.ToolSession;
045: import org.sakaiproject.tool.cover.SessionManager;
046: import org.sakaiproject.tool.cover.ToolManager;
047: import org.sakaiproject.component.cover.ServerConfigurationService;
048: import org.sakaiproject.entity.cover.EntityManager;
049: import org.sakaiproject.entity.api.Entity;
050: import org.sakaiproject.entity.api.EntityTransferrer;
051: import org.sakaiproject.entity.api.HttpAccess;
052: import org.sakaiproject.entity.api.Reference;
053: import org.sakaiproject.entity.api.ResourceProperties;
054: import org.sakaiproject.exception.IdUnusedException;
055: import org.w3c.dom.DOMException;
056: import org.w3c.dom.Document;
057: import org.w3c.dom.Element;
058: import org.w3c.dom.Node;
059: import org.w3c.dom.NodeList;
060:
061: /**
062: * <p>
063: * BasicNewsService implements the NewsService using the Rome RSS package.
064: * </p>
065: */
066: public class BasicNewsService implements NewsService, EntityTransferrer {
067: /** Our log (commons). */
068: private static Log M_log = LogFactory
069: .getLog(BasicNewsService.class);
070:
071: private static final String TOOL_ID = "sakai.news";
072:
073: private static final String NEWS = "news";
074: private static final String NEWS_ITEM = "news_item";
075: private static final String NEWS_URL = "url";
076: private static final String TOOL_TITLE = "tool_title";
077: private static final String PAGE_TITLE = "page_title";
078: private static final String ARCHIVE_VERSION = "2.4"; // in case new features are added in future exports
079:
080: private static final String VERSION_ATTR = "version";
081: private static final String NEWS_URL_PROP = "channel-url";
082:
083: public static final String ATTR_TOP_REFRESH = "sakai.vppa.top.refresh";
084:
085: // default expiration is 10 minutes (expressed in seconds)
086: protected static final int DEFAULT_EXPIRATION = 10 * 60;
087:
088: // The cache in which channels and news-items are held
089: protected Cache m_storage = null;
090:
091: /**********************************************************************************************************************************************************************************************************************************************************
092: * Constructors, Dependencies and their setter methods
093: *********************************************************************************************************************************************************************************************************************************************************/
094:
095: /** Dependency: MemoryService. */
096: protected MemoryService m_memoryService = null;
097:
098: /**
099: * Dependency: MemoryService.
100: *
101: * @param service
102: * The MemoryService.
103: */
104: public void setMemoryService(MemoryService service) {
105: m_memoryService = service;
106: }
107:
108: /**********************************************************************************************************************************************************************************************************************************************************
109: * Init and Destroy
110: *********************************************************************************************************************************************************************************************************************************************************/
111:
112: /**
113: * Final initialization, once all dependencies are set.
114: */
115: public void init() {
116: try {
117: M_log.info("init()");
118:
119: m_storage = m_memoryService.newCache();
120: } catch (Throwable t) {
121: M_log.warn("init(): ", t);
122: }
123:
124: // register as an entity producer
125: EntityManager.registerEntityProducer(this , REFERENCE_ROOT);
126:
127: } // init
128:
129: /**
130: * Returns to uninitialized state.
131: */
132: public void destroy() {
133: m_storage.destroy();
134: m_storage = null;
135:
136: M_log.info("destroy()");
137: }
138:
139: /**
140: * <p>
141: * Checks whether channel is cached. If not or if it's expired, retrieve the feed and update the cache
142: * </p>
143: *
144: * @param source
145: * The url for the news feed
146: * @exception NewsConnectionException,
147: * for errors making the connection.
148: * @exception NewsFormatException,
149: * for errors in the URL or errors parsing the feed.
150: */
151: protected void updateChannel(String source)
152: throws NewsConnectionException, NewsFormatException {
153: // if channel is expired or not in cache, attempt to update
154: // synchronize this part??? %%%%%%
155: if (!m_storage.containsKey(source)) {
156: BasicNewsChannel channel = new BasicNewsChannel(source);
157: m_storage.put(source, channel, DEFAULT_EXPIRATION);
158: }
159:
160: }
161:
162: /**********************************************************************************************************************************************************************************************************************************************************
163: * NewsService implementation
164: *********************************************************************************************************************************************************************************************************************************************************/
165:
166: /**
167: * Retrieves a list of items from an rss feed.
168: *
169: * @param source
170: * The url for the feed.
171: * @return A list of NewsItem objects retrieved from the feed.
172: * @exception NewsConnectionException,
173: * for errors making the connection.
174: * @exception NewsFormatException,
175: * for errors in the URL or errors parsing the feed.
176: */
177: public List getNewsitems(String source)
178: throws NewsConnectionException, NewsFormatException {
179: // if channel is expired or not in cache, attempt to update
180: updateChannel(source);
181:
182: // return a list of items from the channel
183: Object obj = m_storage.get(source);
184: NewsChannel ch = (NewsChannel) obj;
185: return ch.getNewsitems();
186: }
187:
188: /**
189: * Retrieves a list of items from an rss feed.
190: *
191: * @param source
192: * The url for the feed.
193: * @param filter
194: * A filtering object to accept NewsItems, or null if no filtering is desired.
195: * @return A list of NewsItem objects retrieved from the feed.
196: * @exception NewsConnectionException,
197: * for errors making the connection.
198: * @exception NewsFormatException,
199: * for errors in the URL or errors parsing the feed.
200: */
201: public List getNewsitems(String source, Filter filter)
202: throws NewsConnectionException, NewsFormatException {
203: // if channel is expired or not in cache, attempt to update
204: updateChannel(source);
205:
206: //
207: return ((NewsChannel) m_storage.get(source))
208: .getNewsitems(filter);
209:
210: } // getNewsitems
211:
212: /**
213: * Checks whether an update is available for the rss news feed.
214: *
215: * @param source
216: * The url for the feed.
217: * @return true if update is available, false otherwise
218: * @exception NewsConnectionException,
219: * for errors making the connection.
220: * @exception NewsFormatException,
221: * for errors in the URL or errors parsing the feed.
222: */
223: public boolean isUpdateAvailable(String source) {
224: // %%%%%
225: return true;
226: }
227:
228: /**
229: * Retrieves a list of URLs for NewsChannel objects currently indexed by the service.
230: *
231: * @return A list of NewsChannel objects (possibly empty).
232: */
233: public List getChannels() {
234: return m_storage.getKeys();
235: }
236:
237: /**
238: * Retrieves a NewsChannel object indexed by a URL.
239: *
240: * @param source
241: * The url for the channel.
242: * @return A NewsChannel object (possibly null).
243: * @exception NewsConnectionException,
244: * for errors making the connection.
245: * @exception NewsFormatException,
246: * for errors in the URL or errors parsing the feed.
247: */
248: public NewsChannel getChannel(String source)
249: throws NewsConnectionException, NewsFormatException {
250: updateChannel(source);
251: return (NewsChannel) m_storage.get(source);
252: }
253:
254: /**
255: * Removes a NewsChannel object from the service.
256: *
257: * @param source
258: * The url for the channel.
259: */
260: public void removeChannel(String source) {
261: m_storage.remove(source);
262: }
263:
264: /**********************************************************************************************************************************************************************************************************************************************************
265: * Import/Export
266: *********************************************************************************************************************************************************************************************************************************************************/
267: /**
268: * {@inheritDoc}
269: */
270: public String getEntityUrl(Reference ref) {
271: return null;
272: }
273:
274: /**
275: * {@inheritDoc}
276: */
277: public boolean willArchiveMerge() {
278: return true;
279: }
280:
281: /**
282: * {@inheritDoc}
283: */
284: public boolean willImport() {
285: return true;
286: }
287:
288: /**
289: * {@inheritDoc}
290: */
291: public HttpAccess getHttpAccess() {
292: return null;
293: }
294:
295: /**
296: * {@inheritDoc}
297: */
298: public String getEntityDescription(Reference ref) {
299: return null;
300: }
301:
302: /**
303: * {@inheritDoc}
304: */
305: public ResourceProperties getEntityResourceProperties(Reference ref) {
306: return null;
307: }
308:
309: /**
310: * {@inheritDoc}
311: */
312: public Entity getEntity(Reference ref) {
313: return null;
314: }
315:
316: /**
317: * {@inheritDoc}
318: */
319: public String merge(String siteId, Element root,
320: String archivePath, String fromSiteId, Map attachmentNames,
321: Map userIdTrans, Set userListAllowImport) {
322: Base64 codec = new Base64();
323: M_log.info("merge starts for News...");
324: if (siteId != null && siteId.trim().length() > 0) {
325: try {
326: Site site = SiteService.getSite(siteId);
327: NodeList allChildrenNodes = root.getChildNodes();
328: int length = allChildrenNodes.getLength();
329: for (int i = 0; i < length; i++) {
330: Node siteNode = allChildrenNodes.item(i);
331: if (siteNode.getNodeType() == Node.ELEMENT_NODE) {
332: Element siteElement = (Element) siteNode;
333: if (siteElement.getTagName().equals(NEWS)) {
334: NodeList allContentNodes = siteElement
335: .getChildNodes();
336: int lengthContent = allContentNodes
337: .getLength();
338: for (int j = 0; j < lengthContent; j++) {
339: Node child1 = allContentNodes.item(j);
340: if (child1.getNodeType() == Node.ELEMENT_NODE) {
341: Element contentElement = (Element) child1;
342: if (contentElement.getTagName()
343: .equals(NEWS_ITEM)) {
344: String toolTitle = contentElement
345: .getAttribute(TOOL_TITLE);
346: String trimBody = null;
347: if (toolTitle != null
348: && toolTitle.length() > 0) {
349: trimBody = trimToNull(toolTitle);
350: if (trimBody != null
351: && trimBody
352: .length() > 0) {
353: byte[] decoded = codec
354: .decode(trimBody
355: .getBytes("UTF-8"));
356: toolTitle = new String(
357: decoded,
358: "UTF-8");
359: }
360: }
361:
362: String pageTitle = contentElement
363: .getAttribute(PAGE_TITLE);
364: trimBody = null;
365: if (pageTitle != null
366: && pageTitle.length() > 0) {
367: trimBody = trimToNull(pageTitle);
368: if (trimBody != null
369: && trimBody
370: .length() > 0) {
371: byte[] decoded = codec
372: .decode(trimBody
373: .getBytes("UTF-8"));
374: pageTitle = new String(
375: decoded,
376: "UTF-8");
377: }
378: }
379:
380: String contentUrl = contentElement
381: .getAttribute(NEWS_URL);
382: trimBody = null;
383: if (contentUrl != null
384: && contentUrl.length() > 0) {
385: trimBody = trimToNull(contentUrl);
386: if (trimBody != null
387: && trimBody
388: .length() > 0) {
389: byte[] decoded = codec
390: .decode(trimBody
391: .getBytes("UTF-8"));
392: contentUrl = new String(
393: decoded,
394: "UTF-8");
395: }
396: }
397:
398: if (toolTitle != null
399: && contentUrl != null
400: && toolTitle.length() > 0
401: && contentUrl.length() > 0
402: && pageTitle != null
403: && pageTitle.length() > 0) {
404: Tool tr = ToolManager
405: .getTool(TOOL_ID);
406: SitePage page = site
407: .addPage();
408: page.setTitle(pageTitle);
409: ToolConfiguration tool = page
410: .addTool();
411: tool.setTool(TOOL_ID, tr);
412: tool.setTitle(toolTitle);
413: tool
414: .getPlacementConfig()
415: .setProperty(
416: NEWS_URL_PROP,
417: contentUrl);
418: }
419: }
420: }
421: }
422: }
423: }
424: }
425: SiteService.save(site);
426: ToolSession session = SessionManager
427: .getCurrentToolSession();
428:
429: if (session.getAttribute(ATTR_TOP_REFRESH) == null) {
430: session
431: .setAttribute(ATTR_TOP_REFRESH,
432: Boolean.TRUE);
433: }
434: } catch (Exception e) {
435: M_log.error("errors in merge for BasicNewsService");
436: e.printStackTrace();
437: }
438: }
439:
440: return null;
441: }
442:
443: /**
444: * {@inheritDoc}
445: */
446: public String archive(String siteId, Document doc, Stack stack,
447: String arg3, List attachments) {
448: StringBuffer results = new StringBuffer();
449: Base64 codec = new Base64();
450: try {
451: int count = 0;
452: results.append("archiving " + getLabel() + " context "
453: + Entity.SEPARATOR + siteId + Entity.SEPARATOR
454: + SiteService.MAIN_CONTAINER + ".\n");
455:
456: // get the default news url
457: String defaultUrl = ServerConfigurationService
458: .getString("news.feedURL");
459:
460: // start with an element with our very own (service) name
461: Element element = doc.createElement(SERVICE_NAME);
462: element.setAttribute(VERSION_ATTR, ARCHIVE_VERSION);
463: ((Element) stack.peek()).appendChild(element);
464: stack.push(element);
465: if (siteId != null && siteId.trim().length() > 0) {
466: Element newsEl = doc.createElement(NEWS);
467:
468: Site site = SiteService.getSite(siteId);
469: List sitePages = site.getPages();
470:
471: if (sitePages != null && !sitePages.isEmpty()) {
472: Iterator pageIter = sitePages.iterator();
473: while (pageIter.hasNext()) {
474: SitePage currPage = (SitePage) pageIter.next();
475:
476: List toolList = currPage.getTools();
477: Iterator toolIter = toolList.iterator();
478: while (toolIter.hasNext()) {
479: ToolConfiguration toolConfig = (ToolConfiguration) toolIter
480: .next();
481:
482: if (toolConfig.getToolId().equals(TOOL_ID)) {
483: Element newsData = doc
484: .createElement(NEWS_ITEM);
485: count++;
486: //There will only be a url property if the user updated the default URL
487: String newsUrl = toolConfig
488: .getPlacementConfig()
489: .getProperty(NEWS_URL_PROP);
490: if (newsUrl == null
491: || newsUrl.length() <= 0) {
492: // news item is using default url
493: newsUrl = defaultUrl;
494: }
495: String toolTitle = toolConfig
496: .getTitle();
497: String pageTitle = currPage.getTitle();
498:
499: try {
500: String encoded = new String(
501: codec.encode(newsUrl
502: .getBytes("UTF-8")),
503: "UTF-8");
504: newsData.setAttribute(NEWS_URL,
505: encoded);
506: } catch (Exception e) {
507: M_log
508: .warn("Encode News URL - "
509: + e);
510: }
511:
512: try {
513: String encoded = new String(
514: codec.encode(toolTitle
515: .getBytes("UTF-8")),
516: "UTF-8");
517: newsData.setAttribute(TOOL_TITLE,
518: encoded);
519: } catch (Exception e) {
520: M_log
521: .warn("Encode News Tool Title - "
522: + e);
523: }
524:
525: try {
526: String encoded = new String(
527: codec.encode(pageTitle
528: .getBytes("UTF-8")),
529: "UTF-8");
530: newsData.setAttribute(PAGE_TITLE,
531: encoded);
532: } catch (Exception e) {
533: M_log
534: .warn("Encode News Page Title - "
535: + e);
536: }
537:
538: newsEl.appendChild(newsData);
539: }
540: }
541: }
542: results.append("archiving " + getLabel() + ": ("
543: + count
544: + ") news items archived successfully.\n");
545: }
546:
547: else {
548: results.append("archiving " + getLabel()
549: + ": empty news archived.\n");
550: }
551:
552: ((Element) stack.peek()).appendChild(newsEl);
553: stack.push(newsEl);
554: }
555: stack.pop();
556:
557: } catch (DOMException e) {
558: M_log.error(e.getMessage(), e);
559: } catch (IdUnusedException e) {
560: M_log.error(e.getMessage(), e);
561: } catch (Exception e) {
562: M_log.error(e.getMessage(), e);
563: }
564: return results.toString();
565: }
566:
567: /**
568: * {@inheritDoc}
569: */
570: public void transferCopyEntities(String fromContext,
571: String toContext, List ids) {
572: M_log.debug("news transferCopyEntities");
573: try {
574: // retrieve all of the news tools to copy
575: Site fromSite = SiteService.getSite(fromContext);
576: Site toSite = SiteService.getSite(toContext);
577:
578: List fromSitePages = fromSite.getPages();
579:
580: if (fromSitePages != null && !fromSitePages.isEmpty()) {
581: Iterator pageIter = fromSitePages.iterator();
582: while (pageIter.hasNext()) {
583: SitePage currPage = (SitePage) pageIter.next();
584:
585: List toolList = currPage.getTools();
586: Iterator toolIter = toolList.iterator();
587: while (toolIter.hasNext()) {
588: ToolConfiguration toolConfig = (ToolConfiguration) toolIter
589: .next();
590: String toolId = toolConfig.getToolId();
591:
592: if (toolId.equals(TOOL_ID)) {
593: String newsUrl = toolConfig
594: .getPlacementConfig().getProperty(
595: NEWS_URL_PROP);
596: String toolTitle = toolConfig.getTitle();
597: String pageTitle = currPage.getTitle();
598:
599: if (toolTitle != null
600: && toolTitle.length() > 0
601: && pageTitle != null
602: && pageTitle.length() > 0) {
603: Tool tr = ToolManager.getTool(TOOL_ID);
604: SitePage page = toSite.addPage();
605: page.setTitle(pageTitle);
606: ToolConfiguration tool = page.addTool();
607: tool.setTool(TOOL_ID, tr);
608: tool.setTitle(toolTitle);
609: if (newsUrl != null) {
610: tool.getPlacementConfig()
611: .setProperty(NEWS_URL_PROP,
612: newsUrl);
613: }
614: }
615: }
616: }
617: }
618: }
619: SiteService.save(toSite);
620: ToolSession session = SessionManager
621: .getCurrentToolSession();
622:
623: if (session.getAttribute(ATTR_TOP_REFRESH) == null) {
624: session.setAttribute(ATTR_TOP_REFRESH, Boolean.TRUE);
625: }
626: } catch (Exception any) {
627: M_log
628: .warn(
629: "transferCopyEntities(): exception in handling news data: ",
630: any);
631: }
632: }
633:
634: /**
635: * {@inheritDoc}
636: */
637: public String getLabel() {
638: return "news";
639: }
640:
641: /**
642: * {@inheritDoc}
643: */
644: public Collection getEntityAuthzGroups(Reference ref) {
645: return null;
646: }
647:
648: /**
649: * {@inheritDoc}
650: */
651: public Collection getEntityAuthzGroups(Reference ref, String userId) {
652: return null;
653: }
654:
655: /**
656: * {@inheritDoc}
657: */
658: public String[] myToolIds() {
659: String[] toolIds = { TOOL_ID };
660: return toolIds;
661: }
662:
663: /**
664: * {@inheritDoc}
665: */
666: public boolean parseEntityReference(String reference, Reference ref) {
667: return false;
668: }
669:
670: public String trimToNull(String value) {
671: if (value == null)
672: return null;
673: value = value.trim();
674: if (value.length() == 0)
675: return null;
676: return value;
677: }
678: }
|