From 69cec927b9bd92da88ec13cc7a8364178cd32938 Mon Sep 17 00:00:00 2001 From: Chris Goldthorpe Date: Mon, 12 Oct 2009 18:35:18 +0000 Subject: Bug 278290 – [Intro] Make the RSS news reader reusable: --- org.eclipse.ui.intro/META-INF/MANIFEST.MF | 2 +- .../ui/internal/intro/impl/IntroPlugin.java | 74 ++- .../eclipse/ui/internal/intro/impl/Messages.java | 8 + .../ui/internal/intro/impl/Messages.properties | 8 + .../intro/contentproviders/EclipseRSSViewer.java | 521 +++++++++++++++++++++ 5 files changed, 611 insertions(+), 2 deletions(-) create mode 100644 org.eclipse.ui.intro/src/org/eclipse/ui/intro/contentproviders/EclipseRSSViewer.java (limited to 'org.eclipse.ui.intro') diff --git a/org.eclipse.ui.intro/META-INF/MANIFEST.MF b/org.eclipse.ui.intro/META-INF/MANIFEST.MF index 16d7c2ca7..fb149d0eb 100644 --- a/org.eclipse.ui.intro/META-INF/MANIFEST.MF +++ b/org.eclipse.ui.intro/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: %plugin_name Bundle-SymbolicName: org.eclipse.ui.intro; singleton:=true -Bundle-Version: 3.3.100.qualifier +Bundle-Version: 3.4.0.qualifier Bundle-Activator: org.eclipse.ui.internal.intro.impl.IntroPlugin Bundle-Vendor: %provider_name Bundle-Localization: plugin diff --git a/org.eclipse.ui.intro/src/org/eclipse/ui/internal/intro/impl/IntroPlugin.java b/org.eclipse.ui.intro/src/org/eclipse/ui/internal/intro/impl/IntroPlugin.java index 2e22e6805..e359b4459 100644 --- a/org.eclipse.ui.intro/src/org/eclipse/ui/internal/intro/impl/IntroPlugin.java +++ b/org.eclipse.ui.intro/src/org/eclipse/ui/internal/intro/impl/IntroPlugin.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2004, 2008 IBM Corporation and others. + * Copyright (c) 2004, 2009 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 @@ -10,7 +10,9 @@ *******************************************************************************/ package org.eclipse.ui.internal.intro.impl; +import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Platform; +import org.eclipse.core.runtime.Status; import org.eclipse.jface.resource.ImageRegistry; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.internal.intro.impl.model.IntroModelRoot; @@ -27,6 +29,12 @@ import org.osgi.framework.BundleContext; public class IntroPlugin extends AbstractUIPlugin { public static final String PLUGIN_ID = "org.eclipse.ui.intro"; //$NON-NLS-1$ + // Debug control variables + public static boolean LOG_WARN = + "true".equalsIgnoreCase(Platform.getDebugOption(PLUGIN_ID+"/debug/warn")); //$NON-NLS-1$ //$NON-NLS-2$ + public static boolean LOG_INFO = + "true".equalsIgnoreCase(Platform.getDebugOption(PLUGIN_ID+"/debug/info")); //$NON-NLS-1$ //$NON-NLS-2$ + // The static shared instance. private static IntroPlugin inst; @@ -194,5 +202,69 @@ public class IntroPlugin extends AbstractUIPlugin { this.uiCreationStartTime = uiCreationStartTime; } + /** + * Logs an Error message. To print errors to console, + * run eclipse with the -console -consolelog arguments + */ + public static synchronized void logError(String message) { + logError(message,null); + } + + /** + * Logs an Error message with an exception. To print errors to console, + * run eclipse with the -console -consolelog arguments + */ + public static synchronized void logError(String message, Throwable ex) { + if (message == null){ + message = ""; //$NON-NLS-1$ + } + Status errorStatus = new Status(IStatus.ERROR, PLUGIN_ID, message, ex); + IntroPlugin.getDefault().getLog().log(errorStatus); + } + + + /** + * Logs a Warning message with an exception. To print warnings to console, + * run eclipse with the -console -consolelog arguments + * + * Only logs if the following conditions are true: + * -debug switch is enabled at the command line + * .options file is placed at the eclipse work directory with the contents: + * com.ibm.ccl.welcome.bits/debug=true + * com.ibm.ccl.welcome.bits/debug/warn=true + */ + public static synchronized void logWarning(String message) { + logWarning(message,null); + } + + + public static synchronized void logWarning(String message,Throwable ex) { + if (IntroPlugin.getDefault().isDebugging() && LOG_WARN) { + if (message == null) + message = ""; //$NON-NLS-1$ + Status warningStatus = new Status(IStatus.WARNING, PLUGIN_ID, + IStatus.OK, message, ex); + getDefault().getLog().log(warningStatus); + } + } + + /** + * Logs a debug message. To print messages to console, + * run eclipse with the -console -consolelog arguments + * + * Only logs if the following conditions are true: + * -debug switch is enabled at the command line + * .options file is placed at the eclipse work directory with the contents: + * com.ibm.ccl.welcome.bits/debug=true + * com.ibm.ccl.welcome.bits/debug/info=true + */ + public static synchronized void logDebug(String message) { + if (IntroPlugin.getDefault().isDebugging() && LOG_INFO) { + if (message == null) + message = ""; //$NON-NLS-1$ + Status status = new Status(IStatus.INFO, PLUGIN_ID,message); + getDefault().getLog().log(status); + } + } } diff --git a/org.eclipse.ui.intro/src/org/eclipse/ui/internal/intro/impl/Messages.java b/org.eclipse.ui.intro/src/org/eclipse/ui/internal/intro/impl/Messages.java index 700a65417..f6d8bc137 100644 --- a/org.eclipse.ui.intro/src/org/eclipse/ui/internal/intro/impl/Messages.java +++ b/org.eclipse.ui.intro/src/org/eclipse/ui/internal/intro/impl/Messages.java @@ -58,4 +58,12 @@ public class Messages extends NLS { //Always Welcome Checkbox public static String AlwaysWelcomeCheckbox_Text; + //Eclipse RSS Viewer + public static String RSS_Subscribe; + public static String RSS_Loading; + public static String RSS_No_news_please_visit; + public static String RSS_Reading; + public static String RSS_No_news; + public static String RSS_Malformed_feed; + } diff --git a/org.eclipse.ui.intro/src/org/eclipse/ui/internal/intro/impl/Messages.properties b/org.eclipse.ui.intro/src/org/eclipse/ui/internal/intro/impl/Messages.properties index c827ee65a..969253c1c 100644 --- a/org.eclipse.ui.intro/src/org/eclipse/ui/internal/intro/impl/Messages.properties +++ b/org.eclipse.ui.intro/src/org/eclipse/ui/internal/intro/impl/Messages.properties @@ -58,3 +58,11 @@ IntroPart_openExternal_tooltip = Open External Browser #Always welcome checkbox AlwaysWelcomeCheckbox_Text=Always show Welcome at start up + +#Eclipse RSS Viewer +RSS_Subscribe=Subscribe +RSS_Loading=Loading the news.... +RSS_No_news_please_visit=No news. Please visit: +RSS_Reading=Reading the news.... +RSS_No_news=No news at this time. +RSS_Malformed_feed=An RSS feed "{0}" is malformed. News from this site is temporarily unavailable. diff --git a/org.eclipse.ui.intro/src/org/eclipse/ui/intro/contentproviders/EclipseRSSViewer.java b/org.eclipse.ui.intro/src/org/eclipse/ui/intro/contentproviders/EclipseRSSViewer.java new file mode 100644 index 000000000..1b2fbb878 --- /dev/null +++ b/org.eclipse.ui.intro/src/org/eclipse/ui/intro/contentproviders/EclipseRSSViewer.java @@ -0,0 +1,521 @@ +/******************************************************************************* + * Copyright (c) 2009 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.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; + +/** + * Content provider which allows a news reader to be included in intro + * @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 HashMap params; + + private IIntroContentProviderSite site; + + private boolean disposed; + + private String id; + + private List 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 + */ + 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 + */ + 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("

"); //$NON-NLS-1$ + out.print(Messages.RSS_Loading); + out.println("

"); //$NON-NLS-1$ + } else { + if (items.size() > 0) { + out.println(""); //$NON-NLS-1$ + } else { + out.print("

"); //$NON-NLS-1$ + out.print(Messages.RSS_No_news_please_visit); + out.print(" "); //$NON-NLS-1$ + out.print(getParameter("no_news_text")); //$NON-NLS-1$ + out.print(""); //$NON-NLS-1$ + out.println("

"); //$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("

"); //$NON-NLS-1$ + out.println(""); //$NON-NLS-1$ + out.println(Messages.RSS_Subscribe); + out.println(""); //$NON-NLS-1$ + out.println("

"); //$NON-NLS-1$ + } + } + } + + /** + * Create widgets to display the newsreader when using the SWT presentation + */ + 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() { + public void linkActivated(HyperlinkEvent e) { + doNavigate((String) e.getHref()); + } + }); + bulletImage = createImage(new Path("icons/arrow.gif")); //$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("
"); //$NON-NLS-1$ + + + if (items==null) + createNewsItems(); + + if (items == null || threadRunning) { + buffer.append("

"); //$NON-NLS-1$ + buffer.append(Messages.RSS_Loading); + buffer.append("

"); //$NON-NLS-1$ + } else { + if (items.size() > 0) { + for (int i = 0; i < items.size(); i++) { + NewsItem item = (NewsItem) items.get(i); + buffer.append("
  • "); //$NON-NLS-1$ + buffer.append(""); //$NON-NLS-1$ + buffer.append(item.label); + buffer.append(""); //$NON-NLS-1$ + buffer.append("
  • "); //$NON-NLS-1$ + + } + } else { + + buffer.append("

    "); //$NON-NLS-1$ + buffer.append(Messages.RSS_No_news); + buffer.append("

    "); //$NON-NLS-1$ + } + } + + buffer.append("
    "); //$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; + } + } + + 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(), + new Runnable() { + public void run() { + 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 { + public void run() { + // important: don't do the work if the + // part gets disposed in the process + if (disposed) + return; + + PlatformUI.getWorkbench().getDisplay().syncExec(new Runnable() { + public void run() { + 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 stack = new Stack(); + private StringBuffer buf; + private NewsItem item; + + 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(); + } + } + + 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; + } + } + } + } + + 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 HashMap setParams(String query) { + HashMap _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{ + + 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 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 + { + public void run() + { + try{ + Thread.sleep(SOCKET_TIMEOUT); + }catch(Exception ex){ + IntroPlugin.logError("Timeout failed.", ex); //$NON-NLS-1$ + } + if (threadRunning) + { + threadRunning = false; + refresh(); + } + } + } +} -- cgit v1.2.3