/******************************************************************************* * Copyright (c) 2009, 2016 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.eclipse.ui.intro.contentproviders; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Stack; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import org.eclipse.core.runtime.FileLocator; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Platform; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.osgi.util.NLS; import org.eclipse.swt.custom.BusyIndicator; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Composite; import org.eclipse.ui.PartInitException; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.browser.IWorkbenchBrowserSupport; import org.eclipse.ui.forms.events.HyperlinkAdapter; import org.eclipse.ui.forms.events.HyperlinkEvent; import org.eclipse.ui.forms.widgets.FormText; import org.eclipse.ui.forms.widgets.FormToolkit; import org.eclipse.ui.internal.intro.impl.IntroPlugin; import org.eclipse.ui.internal.intro.impl.Messages; import org.eclipse.ui.intro.config.IIntroContentProvider; import org.eclipse.ui.intro.config.IIntroContentProviderSite; import org.eclipse.ui.intro.config.IIntroURL; import org.eclipse.ui.intro.config.IntroURLFactory; import org.osgi.framework.Bundle; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; /** * A content provider which allows a news reader to be included in dynamic intro content. * <p> * The id for the contentProvider tag must consist of the following attributes. Each of these attributes must be separated by '##'. * <ul> * <TABLE CELLPADDING=6 FRAME=BOX> * * <THEAD> * <TR> <TH>Attribute</TH> <TH>Description</TH> </TR> * </THEAD> * * <TBODY> * <TR> <TD>url</TD> <TD>RSS news feed url</TD> </TR> * <TR> <TD>welcome_items</TD> <TD>Number of news feed to be displayed</TD> </TR> * <TR> <TD>no_news_url</TD> <TD>Alternative url for news feed</TD> </TR> * <TR> <TD>no_news_text</TD> <TD>Text for the alternative url</TD> </TR> * </TBODY> * * </TABLE> * </ul> * For example: * <p> * <contentProvider <br> * <ul> * id="url=http://www.eclipse.org/home/eclipsenews.rss##welcome_items=5##no_news_url=http://www.eclipse.org/community/##no_news_text=Welcome to the Eclipse Community Page" <br> * pluginId="org.eclipse.ui.intro" <br> * class="org.eclipse.ui.intro.contentproviders.EclipseRSSViewer"> <br> * </ul> * </contentProvider> * </p> * * @since 3.4 */ public class EclipseRSSViewer implements IIntroContentProvider { private final int SOCKET_TIMEOUT = 6000; //milliseconds private static final String INTRO_SHOW_IN_BROWSER = "http://org.eclipse.ui.intro/openBrowser?url="; //$NON-NLS-1$ private static final String HREF_BULLET = "bullet"; //$NON-NLS-1$ private Map<String, String> params; private IIntroContentProviderSite site; private boolean disposed; private String id; private List<NewsItem> items; private Composite parent; private FormToolkit toolkit; private FormText formText; private Image bulletImage; private boolean threadRunning = false; /** * Initialize the content provider * @param site an object which allows rcontainer reflows to be requested */ @Override public void init(IIntroContentProviderSite site) { this.site = site; refresh(); } /** * Create the html content for this newsreader * @param id * @param out a writer where the html will be written */ @Override public void createContent(String id, PrintWriter out) { if (disposed) return; this.id = id; params = setParams(id); if (items==null) createNewsItems(); if (items == null || threadRunning) { out.print("<p class=\"status-text\">"); //$NON-NLS-1$ out.print(Messages.RSS_Loading); out.println("</p>"); //$NON-NLS-1$ } else { if (items.size() > 0) { out.println("<ul id=\"news-feed\" class=\"news-list\">"); //$NON-NLS-1$ for (NewsItem item : items) { out.print("<li>"); //$NON-NLS-1$ out.print("<a class=\"topicList\" href=\""); //$NON-NLS-1$ out.print(createExternalURL(item.url)); out.print("\">"); //$NON-NLS-1$ out.print(item.label); out.print("</a>"); //$NON-NLS-1$ out.print("</li>\n"); //$NON-NLS-1$ } out.println("</ul>"); //$NON-NLS-1$ } else { out.print("<p class=\"status-text\">"); //$NON-NLS-1$ out.print(Messages.RSS_No_news_please_visit); out.print(" <a href=\""); //$NON-NLS-1$ out.print(createExternalURL(getParameter("no_news_url"))); //$NON-NLS-1$ out.print("\">"); //$NON-NLS-1$ out.print(getParameter("no_news_text")); //$NON-NLS-1$ out.print("</a>"); //$NON-NLS-1$ out.println("</p>"); //$NON-NLS-1$ } URL url = null; try { url = new URL(getParameter("url")); //$NON-NLS-1$ } catch (MalformedURLException e) { IntroPlugin.logError("Bad URL: "+url, e); //$NON-NLS-1$ } if (url != null) { out.println("<p><span class=\"rss-feed-link\">"); //$NON-NLS-1$ out.println("<a href=\""); //$NON-NLS-1$ out.println(createExternalURL(url.toString())); out.println("\">"); //$NON-NLS-1$ out.println(Messages.RSS_Subscribe); out.println("</a>"); //$NON-NLS-1$ out.println("</span></p>"); //$NON-NLS-1$ } } } /** * Create widgets to display the newsreader when using the SWT presentation */ @Override public void createContent(String id, Composite parent, FormToolkit toolkit) { if (disposed) return; this.id = id; params = setParams(id); if (formText == null) { // a one-time pass formText = toolkit.createFormText(parent, true); formText.addHyperlinkListener(new HyperlinkAdapter() { @Override public void linkActivated(HyperlinkEvent e) { doNavigate((String) e.getHref()); } }); bulletImage = createImage(new Path("icons/arrow.png")); //$NON-NLS-1$ if (bulletImage != null) formText.setImage(HREF_BULLET, bulletImage); this.parent = parent; this.toolkit = toolkit; this.id = id; params = setParams(id); } StringBuffer buffer = new StringBuffer(); buffer.append("<form>"); //$NON-NLS-1$ if (items==null) createNewsItems(); if (items == null || threadRunning) { buffer.append("<p>"); //$NON-NLS-1$ buffer.append(Messages.RSS_Loading); buffer.append("</p>"); //$NON-NLS-1$ } else { if (items.size() > 0) { for (int i = 0; i < items.size(); i++) { NewsItem item = (NewsItem) items.get(i); buffer.append("<li style=\"image\" value=\""); //$NON-NLS-1$ buffer.append(HREF_BULLET); buffer.append("\">"); //$NON-NLS-1$ buffer.append("<a href=\""); //$NON-NLS-1$ buffer.append(createExternalURL(item.url)); buffer.append("\">"); //$NON-NLS-1$ buffer.append(item.label); buffer.append("</a>"); //$NON-NLS-1$ buffer.append("</li>"); //$NON-NLS-1$ } } else { buffer.append("<p>"); //$NON-NLS-1$ buffer.append(Messages.RSS_No_news); buffer.append("</p>"); //$NON-NLS-1$ } } buffer.append("</form>"); //$NON-NLS-1$ String text = buffer.toString(); text = text.replaceAll("&{1}", "&"); //$NON-NLS-1$ //$NON-NLS-2$ formText.setText(text, true, false); } private String createExternalURL(String url) { try { return INTRO_SHOW_IN_BROWSER + URLEncoder.encode(url, "UTF-8"); //$NON-NLS-1$ } catch (UnsupportedEncodingException e) { return INTRO_SHOW_IN_BROWSER + url; } } @Override public void dispose() { if (bulletImage != null) { bulletImage.dispose(); bulletImage = null; } disposed = true; } /** * Method is responsible for gathering RSS data. * * Kicks off 2 threads: * * The first (ContentThread) is to actually query the feeds URL to find RSS entries. * When it finishes, it calls a refresh to display the entires it found (if any). * * [Esc RATLC00319786] * The second (TimeoutThread) waits for SOCKET_TIMEOUT ms to see if the content thread * has finished reading RSS. If it has finished, nothing further happens. If it has * not finished, the TimeoutThread sets the threadRunning boolean to false and refreshes * the page (basically telling the UI that no content could be found, and removes * the 'Loading...' text). * */ private void createNewsItems() { ContentThread contentThread = new ContentThread(); contentThread.start(); TimeoutThread timeThread = new TimeoutThread(); timeThread.start(); } /** * Reflows the page using an UI thread. */ private void refresh() { Thread newsWorker = new Thread(new NewsFeed()); newsWorker.start(); } private Image createImage(IPath path) { Bundle bundle = Platform.getBundle(IntroPlugin.PLUGIN_ID); URL url = FileLocator.find(bundle, path, null); try { url = FileLocator.toFileURL(url); ImageDescriptor desc = ImageDescriptor.createFromURL(url); return desc.createImage(); } catch (IOException e) { return null; } } private void doNavigate(final String url) { BusyIndicator.showWhile(PlatformUI.getWorkbench().getDisplay(), () -> { IIntroURL introUrl = IntroURLFactory .createIntroURL(url); if (introUrl != null) { // execute the action embedded in the IntroURL introUrl.execute(); return; } // delegate to the browser support openBrowser(url); }); } private void openBrowser(String href) { try { URL url = new URL(href); IWorkbenchBrowserSupport support = PlatformUI.getWorkbench() .getBrowserSupport(); support.getExternalBrowser().openURL(url); } catch (PartInitException e) { } catch (MalformedURLException e) { } } static class NewsItem { String label; String url; void setLabel(String label) { this.label = label; } void setUrl(String url) { this.url = url; } } class NewsFeed implements Runnable { @Override public void run() { // important: don't do the work if the // part gets disposed in the process if (disposed) return; PlatformUI.getWorkbench().getDisplay().syncExec(() -> { if (parent != null) { // we must recreate the content // for SWT because we will use // a gentle incremental reflow. // HTML reflow will simply reload the page. createContent(id, parent, toolkit); // reflow(formText); } site.reflow(EclipseRSSViewer.this, true); }); } } /** * Handles RSS XML and populates the items list with at most * MAX_NEWS_ITEMS items. */ private class RSSHandler extends DefaultHandler { private static final String ELEMENT_RSS = "rss"; //$NON-NLS-1$ private static final String ELEMENT_CHANNEL = "channel"; //$NON-NLS-1$ private static final String ELEMENT_ITEM = "item"; //$NON-NLS-1$ private static final String ELEMENT_TITLE = "title"; //$NON-NLS-1$ private static final String ELEMENT_LINK = "link"; //$NON-NLS-1$ private Stack<String> stack = new Stack<>(); private StringBuffer buf; private NewsItem item; @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { stack.push(qName); // it's a title/link in an item if ((ELEMENT_TITLE.equals(qName) || ELEMENT_LINK.equals(qName)) && (item != null)) { // prepare the buffer; we're expecting chars buf = new StringBuffer(); } // it's an item in a channel in rss else if (ELEMENT_ITEM.equals(qName) && (ELEMENT_CHANNEL.equals(stack.get(1))) && (ELEMENT_RSS.equals(stack.get(0))) && (stack.size() == 3) && (items.size() < Integer .parseInt(getParameter("welcome_items")))) { //$NON-NLS-1$ // prepare the item item = new NewsItem(); } } @Override public void endElement(String uri, String localName, String qName) throws SAXException { stack.pop(); if (item != null) { if (buf != null) { if (ELEMENT_TITLE.equals(qName)) { item.setLabel(buf.toString().trim()); buf = null; } else if (ELEMENT_LINK.equals(qName)) { item.setUrl(buf.toString().trim()); buf = null; } } else { if (ELEMENT_ITEM.equals(qName)) { // ensure we have a valid item if (item.label != null && item.label.length() > 0 && item.url != null && item.url.length() > 0) { items.add(item); } item = null; } } } } @Override public void characters(char[] ch, int start, int length) throws SAXException { // were we expecting chars? if (buf != null) { buf.append(new String(ch, start, length)); } } } private Map<String, String> setParams(String query) { Map<String, String> _params = new HashMap<>(); //String[] t = _query.split("?"); //String query = t[1]; if (query != null && query.length() > 1) { //String qs = query.substring(1); String[] kvPairs = query.split("##"); //$NON-NLS-1$ for (int i = 0; i < kvPairs.length; i++) { String[] kv = kvPairs[i].split("=", 2); //$NON-NLS-1$ if (kv.length > 1) { _params.put(kv[0], kv[1]); } else { _params.put(kv[0], ""); //$NON-NLS-1$ } } } return _params; } private String getParameter(String name) { return (String) params.get(name); } private class ContentThread extends Thread{ @Override public void run() { threadRunning = true; items = Collections.synchronizedList(new ArrayList<>()); InputStream in = null; try { IntroPlugin.logDebug("Open Connection: "+getParameter("url")); //$NON-NLS-1$ //$NON-NLS-2$ URL url = new URL(getParameter("url")); //$NON-NLS-1$ URLConnection conn = url.openConnection(); // set connection timeout to 6 seconds setTimeout(conn, SOCKET_TIMEOUT); // Connection timeout to 6 seconds conn.connect(); in = url.openStream(); SAXParser parser = SAXParserFactory.newInstance().newSAXParser(); parser.parse(in, new RSSHandler()); refresh(); } catch (Exception e) { IntroPlugin.logError( NLS.bind( Messages.RSS_Malformed_feed, getParameter("url"))); //$NON-NLS-1$ refresh(); } finally { try { if (in != null) { in.close(); } } catch (IOException e) { } threadRunning = false; } } private void setTimeout(URLConnection conn, int milliseconds) { Class<? extends URLConnection> conClass = conn.getClass(); try { Method timeoutMethod = conClass.getMethod( "setConnectTimeout", new Class[]{ int.class } ); //$NON-NLS-1$ timeoutMethod.invoke(conn, new Object[] { new Integer(milliseconds)} ); } catch (Exception e) { // If running on a 1.4 JRE an exception is expected, fall through } } } private class TimeoutThread extends Thread { @Override public void run() { try{ Thread.sleep(SOCKET_TIMEOUT); }catch(Exception ex){ IntroPlugin.logError("Timeout failed.", ex); //$NON-NLS-1$ } if (threadRunning) { threadRunning = false; refresh(); } } } }