/********************************************************************************** * $URL: https://source.sakaiproject.org/svn/web/trunk/news-impl/impl/src/java/org/sakaiproject/news/impl/BasicNewsService.java $ * $Id: BasicNewsService.java 129461 2013-09-09 18:57:14Z holladay@longsight.com $ *********************************************************************************** * * Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008 The Sakai Foundation * * Licensed under the Educational Community License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.opensource.org/licenses/ECL-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * **********************************************************************************/ package org.sakaiproject.news.impl; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Stack; import java.util.Vector; import org.apache.commons.codec.binary.Base64; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.sakaiproject.javax.Filter; import org.sakaiproject.memory.api.Cache; import org.sakaiproject.memory.api.MemoryService; import org.sakaiproject.news.api.NewsChannel; import org.sakaiproject.news.api.NewsConnectionException; import org.sakaiproject.news.api.NewsFormatException; import org.sakaiproject.news.api.NewsService; import org.sakaiproject.site.api.Site; import org.sakaiproject.site.api.SitePage; import org.sakaiproject.site.api.ToolConfiguration; import org.sakaiproject.site.cover.SiteService; import org.sakaiproject.tool.api.Tool; import org.sakaiproject.tool.api.ToolSession; import org.sakaiproject.tool.cover.SessionManager; import org.sakaiproject.tool.cover.ToolManager; import org.sakaiproject.component.cover.ServerConfigurationService; import org.sakaiproject.entity.cover.EntityManager; import org.sakaiproject.entity.api.Entity; import org.sakaiproject.entity.api.EntityTransferrer; import org.sakaiproject.entity.api.HttpAccess; import org.sakaiproject.entity.api.Reference; import org.sakaiproject.entity.api.ResourceProperties; import org.sakaiproject.exception.IdUnusedException; import org.w3c.dom.DOMException; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * <p> * BasicNewsService implements the NewsService using the Rome RSS package. * </p> */ public class BasicNewsService implements NewsService, EntityTransferrer { /** Our log (commons). */ private static Log M_log = LogFactory.getLog(BasicNewsService.class); private static final String TOOL_ID = "sakai.news"; private static final String NEWS = "news"; private static final String NEWS_ITEM = "news_item"; private static final String NEWS_URL = "url"; private static final String TOOL_TITLE = "tool_title"; private static final String PAGE_TITLE = "page_title"; private static final String ARCHIVE_VERSION = "2.4"; // in case new features are added in future exports private static final String VERSION_ATTR = "version"; private static final String NEWS_URL_PROP = "channel-url"; public static final String ATTR_TOP_REFRESH = "sakai.vppa.top.refresh"; // default expiration is 10 minutes (expressed in seconds) protected static final int DEFAULT_EXPIRATION = 10 * 60; // The cache in which channels and news-items are held protected Cache m_storage = null; /********************************************************************************************************************************************************************************************************************************************************** * Constructors, Dependencies and their setter methods *********************************************************************************************************************************************************************************************************************************************************/ /** Dependency: MemoryService. */ protected MemoryService m_memoryService = null; /** * Dependency: MemoryService. * * @param service * The MemoryService. */ public void setMemoryService(MemoryService service) { m_memoryService = service; } /********************************************************************************************************************************************************************************************************************************************************** * Init and Destroy *********************************************************************************************************************************************************************************************************************************************************/ /** * Final initialization, once all dependencies are set. */ public void init() { try { M_log.info("init()"); m_storage = m_memoryService .newCache("org.sakaiproject.news.api.NewsService.cache"); } catch (Throwable t) { M_log.warn("init(): ", t); } // register as an entity producer EntityManager.registerEntityProducer(this, REFERENCE_ROOT); } // init /** * Returns to uninitialized state. */ public void destroy() { m_storage.destroy(); m_storage = null; M_log.info("destroy()"); } /** * <p> * Checks whether channel is cached. If not or if it's expired, retrieve the feed and update the cache * </p> * * @param source * The url for the news feed * @exception NewsConnectionException, * for errors making the connection. * @exception NewsFormatException, * for errors in the URL or errors parsing the feed. */ protected void updateChannel(String source) throws NewsConnectionException, NewsFormatException { // if channel is expired or not in cache, attempt to update // synchronize this part??? %%%%%% if (!m_storage.containsKey(source)) { BasicNewsChannel channel = new BasicNewsChannel(source, getUserAgent()); m_storage.put(source, channel, DEFAULT_EXPIRATION); } } /********************************************************************************************************************************************************************************************************************************************************** * NewsService implementation *********************************************************************************************************************************************************************************************************************************************************/ /** * Retrieves a list of items from an rss feed. * * @param source * The url for the feed. * @return A list of NewsItem objects retrieved from the feed. * @exception NewsConnectionException, * for errors making the connection. * @exception NewsFormatException, * for errors in the URL or errors parsing the feed. */ public List getNewsitems(String source) throws NewsConnectionException, NewsFormatException { // if channel is expired or not in cache, attempt to update updateChannel(source); // return a list of items from the channel Object obj = m_storage.get(source); NewsChannel ch = (NewsChannel) obj; return ch.getNewsitems(); } /** * Retrieves a list of items from an rss feed. * * @param source * The url for the feed. * @param filter * A filtering object to accept NewsItems, or null if no filtering is desired. * @return A list of NewsItem objects retrieved from the feed. * @exception NewsConnectionException, * for errors making the connection. * @exception NewsFormatException, * for errors in the URL or errors parsing the feed. */ public List getNewsitems(String source, Filter filter) throws NewsConnectionException, NewsFormatException { // if channel is expired or not in cache, attempt to update updateChannel(source); // return ((NewsChannel) m_storage.get(source)).getNewsitems(filter); } // getNewsitems /** * Checks whether an update is available for the rss news feed. * * @param source * The url for the feed. * @return true if update is available, false otherwise * @exception NewsConnectionException, * for errors making the connection. * @exception NewsFormatException, * for errors in the URL or errors parsing the feed. */ public boolean isUpdateAvailable(String source) { // %%%%% return true; } /** * Retrieves a list of URLs for NewsChannel objects currently indexed by the service. * * @return A list of NewsChannel objects (possibly empty). */ public List getChannels() { return m_storage.getKeys(); } /** * Retrieves a NewsChannel object indexed by a URL. * * @param source * The url for the channel. * @return A NewsChannel object (possibly null). * @exception NewsConnectionException, * for errors making the connection. * @exception NewsFormatException, * for errors in the URL or errors parsing the feed. */ public NewsChannel getChannel(String source) throws NewsConnectionException, NewsFormatException { updateChannel(source); return (NewsChannel) m_storage.get(source); } /** * Removes a NewsChannel object from the service. * * @param source * The url for the channel. */ public void removeChannel(String source) { m_storage.remove(source); } /********************************************************************************************************************************************************************************************************************************************************** * Import/Export *********************************************************************************************************************************************************************************************************************************************************/ /** * {@inheritDoc} */ public String getEntityUrl(Reference ref) { return null; } /** * {@inheritDoc} */ public boolean willArchiveMerge() { return true; } /** * {@inheritDoc} */ public boolean willImport() { return true; } /** * {@inheritDoc} */ public HttpAccess getHttpAccess() { return null; } /** * {@inheritDoc} */ public String getEntityDescription(Reference ref) { return null; } /** * {@inheritDoc} */ public ResourceProperties getEntityResourceProperties(Reference ref) { return null; } /** * {@inheritDoc} */ public Entity getEntity(Reference ref) { return null; } /** * {@inheritDoc} */ public String merge(String siteId, Element root, String archivePath, String fromSiteId, Map attachmentNames, Map userIdTrans, Set userListAllowImport) { Base64 codec = new Base64(); M_log.info("merge starts for News..."); if (siteId != null && siteId.trim().length() > 0) { try { Site site = SiteService.getSite(siteId); NodeList allChildrenNodes = root.getChildNodes(); int length = allChildrenNodes.getLength(); for (int i = 0; i < length; i++) { Node siteNode = allChildrenNodes.item(i); if (siteNode.getNodeType() == Node.ELEMENT_NODE) { Element siteElement = (Element) siteNode; if (siteElement.getTagName().equals(NEWS)) { NodeList allContentNodes = siteElement.getChildNodes(); int lengthContent = allContentNodes.getLength(); for (int j = 0; j < lengthContent; j++) { Node child1 = allContentNodes.item(j); if (child1.getNodeType() == Node.ELEMENT_NODE) { Element contentElement = (Element) child1; if (contentElement.getTagName().equals(NEWS_ITEM)) { String toolTitle = contentElement.getAttribute(TOOL_TITLE); String trimBody = null; if(toolTitle != null && toolTitle.length() >0) { trimBody = trimToNull(toolTitle); if (trimBody != null && trimBody.length() >0) { byte[] decoded = codec.decode(trimBody.getBytes("UTF-8")); toolTitle = new String(decoded, "UTF-8"); } } String pageTitle = contentElement.getAttribute(PAGE_TITLE); trimBody = null; if(pageTitle != null && pageTitle.length() >0) { trimBody = trimToNull(pageTitle); if (trimBody != null && trimBody.length() >0) { byte[] decoded = codec.decode(trimBody.getBytes("UTF-8")); pageTitle = new String(decoded, "UTF-8"); } } String contentUrl = contentElement.getAttribute(NEWS_URL); trimBody = null; if(contentUrl != null && contentUrl.length() >0) { trimBody = trimToNull(contentUrl); if (trimBody != null && trimBody.length() >0) { byte[] decoded = codec.decode(trimBody.getBytes("UTF-8")); contentUrl = new String(decoded, "UTF-8"); } } if(toolTitle != null && contentUrl != null && toolTitle.length() >0 && contentUrl.length() >0 && pageTitle !=null && pageTitle.length() > 0) { Tool tr = ToolManager.getTool(TOOL_ID); SitePage page = site.addPage(); page.setTitle(pageTitle); ToolConfiguration tool = page.addTool(); tool.setTool(TOOL_ID, tr); tool.setTitle(toolTitle); tool.getPlacementConfig().setProperty(NEWS_URL_PROP, contentUrl); } } } } } } } SiteService.save(site); ToolSession session = SessionManager.getCurrentToolSession(); if (session.getAttribute(ATTR_TOP_REFRESH) == null) { session.setAttribute(ATTR_TOP_REFRESH, Boolean.TRUE); } } catch(Exception e) { M_log.error("errors in merge for BasicNewsService"); } } return null; } /** * {@inheritDoc} */ public String archive(String siteId, Document doc, Stack stack, String arg3, List attachments) { StringBuilder results = new StringBuilder(); Base64 codec = new Base64(); try { int count = 0; results.append("archiving " + getLabel() + " context " + Entity.SEPARATOR + siteId + Entity.SEPARATOR + SiteService.MAIN_CONTAINER + ".\n"); // get the default news url String defaultUrl = ServerConfigurationService.getString("news.feedURL", "http://sakaiproject.org/news-rss-feed"); // start with an element with our very own (service) name Element element = doc.createElement(SERVICE_NAME); element.setAttribute(VERSION_ATTR, ARCHIVE_VERSION); ((Element) stack.peek()).appendChild(element); stack.push(element); if (siteId != null && siteId.trim().length() > 0) { Element newsEl = doc.createElement(NEWS); Site site = SiteService.getSite(siteId); List sitePages = site.getPages(); if (sitePages != null && !sitePages.isEmpty()) { Iterator pageIter = sitePages.iterator(); while (pageIter.hasNext()) { SitePage currPage = (SitePage) pageIter.next(); List toolList = currPage.getTools(); Iterator toolIter = toolList.iterator(); while (toolIter.hasNext()) { ToolConfiguration toolConfig = (ToolConfiguration)toolIter.next(); if (toolConfig.getToolId().equals(TOOL_ID)) { Element newsData = doc.createElement(NEWS_ITEM); count++; //There will only be a url property if the user updated the default URL String newsUrl = toolConfig.getPlacementConfig().getProperty(NEWS_URL_PROP); if (newsUrl == null || newsUrl.length() <= 0) { // news item is using default url newsUrl = defaultUrl; } String toolTitle = toolConfig.getTitle(); String pageTitle = currPage.getTitle(); try { String encoded = new String(codec.encode(newsUrl.getBytes("UTF-8")),"UTF-8"); newsData.setAttribute(NEWS_URL, encoded); } catch(Exception e) { M_log.warn("Encode News URL - " + e); } try { String encoded = new String(codec.encode(toolTitle.getBytes("UTF-8")),"UTF-8"); newsData.setAttribute(TOOL_TITLE, encoded); } catch(Exception e) { M_log.warn("Encode News Tool Title - " + e); } try { String encoded = new String(codec.encode(pageTitle.getBytes("UTF-8")),"UTF-8"); newsData.setAttribute(PAGE_TITLE, encoded); } catch(Exception e) { M_log.warn("Encode News Page Title - " + e); } newsEl.appendChild(newsData); } } } results.append("archiving " + getLabel() + ": (" + count + ") news items archived successfully.\n"); } else { results.append("archiving " + getLabel() + ": empty news archived.\n"); } ((Element) stack.peek()).appendChild(newsEl); stack.push(newsEl); } stack.pop(); } catch (DOMException e) { M_log.error(e.getMessage(), e); } catch (IdUnusedException e) { M_log.error(e.getMessage(), e); } catch (Exception e) { M_log.error(e.getMessage(), e); } return results.toString(); } /** * {@inheritDoc} */ public void transferCopyEntities(String fromContext, String toContext, List ids) { M_log.debug("news transferCopyEntities"); try { // retrieve all of the news tools to copy Site fromSite = SiteService.getSite(fromContext); Site toSite = SiteService.getSite(toContext); List fromSitePages = fromSite.getPages(); if (fromSitePages != null && !fromSitePages.isEmpty()) { Iterator pageIter = fromSitePages.iterator(); while (pageIter.hasNext()) { SitePage currPage = (SitePage) pageIter.next(); List toolList = currPage.getTools(); Iterator toolIter = toolList.iterator(); while (toolIter.hasNext()) { ToolConfiguration toolConfig = (ToolConfiguration)toolIter.next(); String toolId = toolConfig.getToolId(); if (toolId.equals(TOOL_ID)) { String newsUrl = toolConfig.getPlacementConfig().getProperty(NEWS_URL_PROP); String toolTitle = toolConfig.getTitle(); String pageTitle = currPage.getTitle(); // in some cases the new site already has all of this. so make // sure we don't make a duplicate boolean skip = false; String[] toolIds = {TOOL_ID}; Collection<ToolConfiguration> toolConfs = toSite.getTools(TOOL_ID); if (toolConfs != null && !toolConfs.isEmpty()) { for (ToolConfiguration config: toolConfs) { if (config.getToolId().equals(TOOL_ID)) { SitePage p = config.getContainingPage(); if (pageTitle != null && pageTitle.equals(p.getTitle()) && newsUrl != null && newsUrl.equals(config.getPlacementConfig().getProperty(NEWS_URL_PROP))) { skip = true; break; } } } } if(!skip && toolTitle != null && toolTitle.length() >0 && pageTitle !=null && pageTitle.length() > 0) { Tool tr = ToolManager.getTool(TOOL_ID); SitePage page = toSite.addPage(); page.setTitle(pageTitle); ToolConfiguration tool = page.addTool(); tool.setTool(TOOL_ID, tr); tool.setTitle(toolTitle); if (newsUrl != null) { tool.getPlacementConfig().setProperty(NEWS_URL_PROP, newsUrl); } } } } } } SiteService.save(toSite); ToolSession session = SessionManager.getCurrentToolSession(); if (session != null && session.getAttribute(ATTR_TOP_REFRESH) == null) { session.setAttribute(ATTR_TOP_REFRESH, Boolean.TRUE); } } catch (Exception any) { M_log.warn("transferCopyEntities(): exception in handling news data: ", any); } } /** * {@inheritDoc} */ public String getLabel() { return "news"; } /** * {@inheritDoc} */ public Collection getEntityAuthzGroups(Reference ref) { return null; } /** * {@inheritDoc} */ public Collection getEntityAuthzGroups(Reference ref, String userId) { return null; } /** * {@inheritDoc} */ public String[] myToolIds() { String[] toolIds = { TOOL_ID }; return toolIds; } /** * {@inheritDoc} */ public boolean parseEntityReference(String reference, Reference ref) { return false; } public String trimToNull(String value) { if (value == null) return null; value = value.trim(); if (value.length() == 0) return null; return value; } public void transferCopyEntities(String fromContext, String toContext, List ids, boolean cleanup) { try { if(cleanup == true) { // retrieve all of the news tools to remove Site toSite = SiteService.getSite(toContext); List toSitePages = toSite.getPages(); if (toSitePages != null && !toSitePages.isEmpty()) { Vector removePageIds = new Vector(); Iterator pageIter = toSitePages.iterator(); while (pageIter.hasNext()) { SitePage currPage = (SitePage) pageIter.next(); List toolList = currPage.getTools(); Iterator toolIter = toolList.iterator(); while (toolIter.hasNext()) { ToolConfiguration toolConfig = (ToolConfiguration)toolIter.next(); String toolId = toolConfig.getToolId(); if (toolId.equals(TOOL_ID)) { removePageIds.add(toolConfig.getPageId()); } } } for (int i = 0; i < removePageIds.size(); i++) { String removeId = (String) removePageIds.get(i); SitePage sitePage = toSite.getPage(removeId); toSite.removePage(sitePage); } } SiteService.save(toSite); ToolSession session = SessionManager.getCurrentToolSession(); if (session != null && session.getAttribute(ATTR_TOP_REFRESH) == null) { session.setAttribute(ATTR_TOP_REFRESH, Boolean.TRUE); } } } catch (Exception e) { M_log.info("News transferCopyEntities Error" + e); } transferCopyEntities(fromContext, toContext, ids); } /** * Get the user agent to use for web requests. */ protected String getUserAgent() { return "Sakai/"+ ServerConfigurationService.getString("version.sakai")+ " ("+ TOOL_ID+ ")"; } }