blob: fa50f2b883a037720f1784000dfceef38c283eed [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2009, 2017 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>
* &lt;contentProvider <br>
* <ul>
* id=&quot;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&quot; <br>
* pluginId=&quot;org.eclipse.ui.intro&quot; <br>
* class=&quot;org.eclipse.ui.intro.contentproviders.EclipseRSSViewer&quot;&gt; <br>
* </ul>
* &lt;/contentProvider&gt;
* </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);
}
StringBuilder buffer = new StringBuilder();
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 = 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}", "&amp;"); //$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 StringBuilder 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 StringBuilder();
}
// 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 params.get(name);
}
private class ContentThread extends Thread{
@Override
public void run()
{
threadRunning = true;
items = Collections.synchronizedList(new ArrayList<>());
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();
try (InputStream 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 {
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[] { Integer.valueOf(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();
}
}
}
}