/*
* Copyright (c) 2004-2006 Marco Maccaferri 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:
* Marco Maccaferri - initial API and implementation
*/
package org.eclipsetrader.news.internal.connectors;
import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.ValidationEvent;
import javax.xml.bind.ValidationEventHandler;
import javax.xml.transform.stream.StreamSource;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthScope;
import org.eclipse.core.net.proxy.IProxyData;
import org.eclipse.core.net.proxy.IProxyService;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExecutableExtension;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.IJobChangeEvent;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.jobs.JobChangeAdapter;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipsetrader.core.instruments.ISecurity;
import org.eclipsetrader.news.core.IHeadLine;
import org.eclipsetrader.news.core.INewsProvider;
import org.eclipsetrader.news.core.INewsService;
import org.eclipsetrader.news.core.INewsServiceRunnable;
import org.eclipsetrader.news.internal.Activator;
import org.eclipsetrader.news.internal.repository.HeadLine;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.feed.synd.SyndFeed;
import com.sun.syndication.fetcher.impl.FeedFetcherCache;
import com.sun.syndication.fetcher.impl.HashMapFeedInfoCache;
import com.sun.syndication.fetcher.impl.HttpClientFeedFetcher;
public class RSSNewsProvider implements INewsProvider, IExecutableExtension {
public static final String HEADLINES_FILE = "rss.xml"; //$NON-NLS-1$
private String id;
private String name;
private FeedFetcherCache feedInfoCache = HashMapFeedInfoCache.getInstance();
private HttpClientFeedFetcher fetcher = new HttpClientFeedFetcher(feedInfoCache);
private INewsService newsService;
private boolean started;
static private List<HeadLine> oldItems = new ArrayList<HeadLine>();
private JobChangeAdapter jobChangeListener = new JobChangeAdapter() {
@Override
public void done(IJobChangeEvent event) {
IPreferenceStore store = Activator.getDefault().getPreferenceStore();
int interval = store.getInt(Activator.PREFS_UPDATE_INTERVAL);
job.schedule(interval * 60 * 1000);
}
};
private Job job = new Job("RSS Subscriptions") {
@Override
protected IStatus run(IProgressMonitor monitor) {
return jobRunner(monitor);
}
};
public RSSNewsProvider() {
}
/* (non-Javadoc)
* @see org.eclipse.core.runtime.IExecutableExtension#setInitializationData(org.eclipse.core.runtime.IConfigurationElement, java.lang.String, java.lang.Object)
*/
@Override
public void setInitializationData(IConfigurationElement config, String propertyName, Object data) throws CoreException {
id = config.getAttribute("id");
name = config.getAttribute("name");
}
/* (non-Javadoc)
* @see org.eclipsetrader.news.core.INewsProvider#getId()
*/
@Override
public String getId() {
return id;
}
/* (non-Javadoc)
* @see org.eclipsetrader.news.core.INewsProvider#getName()
*/
@Override
public String getName() {
return name;
}
/* (non-Javadoc)
* @see org.eclipsetrader.news.core.INewsProvider#getHeadLines()
*/
@Override
public IHeadLine[] getHeadLines() {
return new IHeadLine[0];
}
/* (non-Javadoc)
* @see org.eclipsetrader.news.core.INewsProvider#start()
*/
@Override
public void start() {
if (!started) {
job.schedule(5 * 1000);
job.addJobChangeListener(jobChangeListener);
started = true;
}
}
/* (non-Javadoc)
* @see org.eclipsetrader.news.core.INewsProvider#stop()
*/
@Override
public void stop() {
if (started) {
job.removeJobChangeListener(jobChangeListener);
job.cancel();
started = false;
}
}
/* (non-Javadoc)
* @see org.eclipsetrader.news.core.INewsProvider#refresh()
*/
@Override
public void refresh() {
job.removeJobChangeListener(jobChangeListener);
job.cancel();
if (started) {
job.addJobChangeListener(jobChangeListener);
}
job.schedule(0);
}
protected IStatus jobRunner(IProgressMonitor monitor) {
final List<IHeadLine> list = new ArrayList<IHeadLine>();
FeedSource[] sources = getActiveSubscriptions();
monitor.beginTask("Updating RSS Subscriptions", sources.length);
try {
for (int i = 0; i < sources.length; i++) {
try {
HeadLine[] headLines = update(new URL(sources[i].getUrl()), sources[i].getName());
for (HeadLine headLine : headLines) {
if (!oldItems.contains(headLine)) {
headLine.setRecent(true);
oldItems.add(headLine);
list.add(headLine);
}
}
} catch (Exception e) {
Status status = new Status(IStatus.WARNING, Activator.PLUGIN_ID, 0, "Error updating headlines from " + sources[i].getUrl(), null); //$NON-NLS-1$
Activator.getDefault().getLog().log(status);
} finally {
monitor.worked(1);
}
}
final INewsService service = getNewsService();
service.runInService(new INewsServiceRunnable() {
@Override
public IStatus run(IProgressMonitor monitor) throws Exception {
service.addHeadLines(list.toArray(new IHeadLine[list.size()]));
return Status.OK_STATUS;
}
}, null);
} finally {
monitor.done();
}
return Status.OK_STATUS;
}
protected void resetRecentFlag() {
final List<HeadLine> updated = new ArrayList<HeadLine>();
for (HeadLine headLine : oldItems) {
if (headLine.isRecent()) {
headLine.setRecent(false);
updated.add(headLine);
}
}
final INewsService service = getNewsService();
service.runInService(new INewsServiceRunnable() {
@Override
public IStatus run(IProgressMonitor monitor) throws Exception {
service.updateHeadLines(updated.toArray(new IHeadLine[updated.size()]));
return Status.OK_STATUS;
}
}, null);
}
protected FeedSource[] getActiveSubscriptions() {
List<FeedSource> list = new ArrayList<FeedSource>();
try {
File file = Activator.getDefault().getStateLocation().append(HEADLINES_FILE).toFile();
if (file.exists()) {
JAXBContext jaxbContext = JAXBContext.newInstance(FeedSource[].class);
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
unmarshaller.setEventHandler(new ValidationEventHandler() {
@Override
public boolean handleEvent(ValidationEvent event) {
Status status = new Status(IStatus.WARNING, Activator.PLUGIN_ID, 0, "Error validating XML: " + event.getMessage(), null); //$NON-NLS-1$
Activator.getDefault().getLog().log(status);
return true;
}
});
JAXBElement<FeedSource[]> element = unmarshaller.unmarshal(new StreamSource(file), FeedSource[].class);
for (FeedSource source : element.getValue()) {
if (source.isEnabled()) {
list.add(source);
}
}
}
} catch (Exception e) {
Status status = new Status(IStatus.WARNING, Activator.PLUGIN_ID, 0, "Error reading RSS subscriptions", null); //$NON-NLS-1$
Activator.getDefault().getLog().log(status);
}
return list.toArray(new FeedSource[list.size()]);
}
private HeadLine[] update(URL feedUrl, String source) {
Map<String, SyndEntry> titles = new HashMap<String, SyndEntry>();
Map<SyndEntry, Set<ISecurity>> entries = new HashMap<SyndEntry, Set<ISecurity>>();
Set<HeadLine> headLines = new HashSet<HeadLine>();
try {
HttpClient client = new HttpClient();
client.getHttpConnectionManager().getParams().setConnectionTimeout(5000);
if (Activator.getDefault() != null) {
BundleContext context = Activator.getDefault().getBundle().getBundleContext();
ServiceReference reference = context.getServiceReference(IProxyService.class.getName());
if (reference != null) {
IProxyService proxy = (IProxyService) context.getService(reference);
IProxyData data = proxy.getProxyDataForHost(feedUrl.getHost(), IProxyData.HTTP_PROXY_TYPE);
if (data != null) {
if (data.getHost() != null) {
client.getHostConfiguration().setProxy(data.getHost(), data.getPort());
}
if (data.isRequiresAuthentication()) {
client.getState().setProxyCredentials(AuthScope.ANY, new UsernamePasswordCredentials(data.getUserId(), data.getPassword()));
}
}
context.ungetService(reference);
}
}
SyndFeed feed = fetcher.retrieveFeed(feedUrl, client);
for (Iterator<?> iter = feed.getEntries().iterator(); iter.hasNext();) {
SyndEntry entry = (SyndEntry) iter.next();
if (titles.containsKey(entry.getTitle())) {
entry = titles.get(entry.getTitle());
}
else {
titles.put(entry.getTitle(), entry);
}
Set<ISecurity> set = entries.get(entry);
if (set == null) {
set = new HashSet<ISecurity>();
entries.put(entry, set);
}
}
for (SyndEntry entry : entries.keySet()) {
Set<ISecurity> set = entries.get(entry);
String url = entry.getLink();
if (url.indexOf('*') != -1) {
url = url.substring(url.indexOf('*') + 1);
}
String title = entry.getTitle();
if (title.endsWith(")")) {
int s = title.lastIndexOf('(');
if (s != -1) {
source = title.substring(s + 1, title.length() - 1);
if (source.startsWith("at ")) {
source = source.substring(3);
}
title = title.substring(0, s - 1).trim();
}
}
HeadLine headLine = new HeadLine(entry.getPublishedDate(), source, title, set.toArray(new ISecurity[set.size()]), url);
headLines.add(headLine);
}
} catch (Exception e) {
Status status = new Status(IStatus.WARNING, Activator.PLUGIN_ID, 0, "Error reading RSS subscription " + feedUrl.toString(), e); //$NON-NLS-1$
Activator.getDefault().getLog().log(status);
}
return headLines.toArray(new HeadLine[headLines.size()]);
}
protected INewsService getNewsService() {
if (newsService == null) {
BundleContext context = Activator.getDefault().getBundle().getBundleContext();
ServiceReference serviceReference = context.getServiceReference(INewsService.class.getName());
if (serviceReference != null) {
newsService = (INewsService) context.getService(serviceReference);
context.ungetService(serviceReference);
}
}
return newsService;
}
}