/* ********************************************************************** ** ** Copyright notice ** ** ** ** (c) 2005-2011 RSSOwl Development Team ** ** http://www.rssowl.org/ ** ** ** ** 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.rssowl.org/legal/epl-v10.html ** ** ** ** A copy is found in the file epl-v10.html and important notices to the ** ** license from the team is found in the textfile LICENSE.txt distributed ** ** in this package. ** ** ** ** This copyright notice MUST APPEAR in all copies of the file! ** ** ** ** Contributors: ** ** RSSOwl Development Team - initial API and implementation ** ** ** ** ********************************************************************** */ package org.rssowl.core.internal.connection; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.Status; import org.osgi.service.url.URLStreamHandlerService; import org.rssowl.core.Owl; import org.rssowl.core.connection.AuthenticationRequiredException; import org.rssowl.core.connection.ConnectionException; import org.rssowl.core.connection.IAbortable; import org.rssowl.core.connection.IConnectionPropertyConstants; import org.rssowl.core.connection.ICredentials; import org.rssowl.core.connection.ICredentialsProvider; import org.rssowl.core.connection.SyncConnectionException; import org.rssowl.core.internal.Activator; import org.rssowl.core.internal.interpreter.json.JSONException; import org.rssowl.core.internal.interpreter.json.JSONObject; import org.rssowl.core.internal.interpreter.json.JSONTokener; import org.rssowl.core.interpreter.ParserException; import org.rssowl.core.persist.IConditionalGet; import org.rssowl.core.persist.IFeed; import org.rssowl.core.persist.IModelFactory; import org.rssowl.core.persist.INews; import org.rssowl.core.util.SyncItem; import org.rssowl.core.util.SyncUtils; import org.rssowl.core.util.Triple; import org.rssowl.core.util.URIUtils; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URI; import java.net.URISyntaxException; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; /** * Extends the {@link DefaultProtocolHandler} dealing with Google Reader * synchronization. The result from loading a feed is a JSON Object that is * passed on to the responsible JSON interpreter service. * * @author bpasero */ public class ReaderProtocolHandler extends DefaultProtocolHandler { /* Some Sync Constants */ private static final String REQUEST_HEADER_USER_AGENT = "User-Agent"; //$NON-NLS-1$ private static final String REQUEST_HEADER_ACCEPT_CHARSET = "Accept-Charset"; //$NON-NLS-1$ private static final String REQUEST_HEADER_AUTHORIZATION = "Authorization"; //$NON-NLS-1$ private static final String UTF_8 = "UTF-8"; //$NON-NLS-1$ private static final String BROWSER_USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; rv:2.0.1) Gecko/20100101 Firefox/4.0.1"; //$NON-NLS-1$ private static final int DEFAULT_ITEM_LIMIT = 200; /* * @see * org.rssowl.core.internal.connection.DefaultProtocolHandler#reload(java. * net.URI, org.eclipse.core.runtime.IProgressMonitor, java.util.Map) */ @SuppressWarnings("unchecked") @Override public Triple<IFeed, IConditionalGet, URI> reload(URI link, IProgressMonitor monitor, Map<Object, Object> properties) throws CoreException { int itemLimit = DEFAULT_ITEM_LIMIT; long dateLimit = 0; /* Look for Sync-Connection specific properties */ if (properties != null) { /* Item Limit */ if (properties.containsKey(IConnectionPropertyConstants.ITEM_LIMIT)) { Object itemLimitObj = properties.get(IConnectionPropertyConstants.ITEM_LIMIT); if (itemLimitObj instanceof Integer) itemLimit = (Integer) itemLimitObj; } /* Date Limit */ if (properties.containsKey(IConnectionPropertyConstants.DATE_LIMIT)) { Object dateLimitObj = properties.get(IConnectionPropertyConstants.DATE_LIMIT); if (dateLimitObj instanceof Long) { dateLimit = (Long) dateLimitObj / 1000; //Google only seems to accept seconds here } } } else properties = new HashMap<Object, Object>(); URI googleLink = readerToGoogle(link, itemLimit, dateLimit); InputStream inS = null; /* First Try: Use shared token */ try { String authToken = handleAuthentication(false, monitor); inS = openGoogleConnection(authToken, googleLink, monitor, properties); } catch (ConnectionException e) { /* Rethrow if this exception is not about Authentication issues */ if (!(e instanceof AuthenticationRequiredException) && !(e instanceof SyncConnectionException)) throw e; /* Return on Cancelation or Shutdown */ if (monitor.isCanceled()) { closeStream(inS, true); return null; } /* Second Try: Obtain fresh token (could be expired) */ String authToken = handleAuthentication(true, monitor); inS = openGoogleConnection(authToken, googleLink, monitor, properties); } /* Return on Cancelation or Shutdown */ if (monitor.isCanceled()) { closeStream(inS, true); return null; } /* Retrieve Conditional Get if present */ IConditionalGet conditionalGet = getConditionalGet(googleLink, inS); /* Return on Cancelation or Shutdown */ if (monitor.isCanceled()) { closeStream(inS, true); return null; } /* Read JSON Object from Response and parse */ InputStreamReader reader = null; boolean isError = false; IModelFactory typesFactory = Owl.getModelFactory(); IFeed feed = typesFactory.createFeed(null, link); feed.setBase(readerToHTTP(link)); try { reader = new InputStreamReader(inS, UTF_8); JSONObject obj = new JSONObject(new JSONTokener(reader)); Owl.getInterpreter().interpretJSONObject(obj, feed); } catch (JSONException e) { isError = true; throw new ParserException(Activator.getDefault().createErrorStatus(e.getMessage(), e)); } catch (IOException e) { isError = true; throw new ParserException(Activator.getDefault().createErrorStatus(e.getMessage(), e)); } finally { try { if (isError && inS instanceof IAbortable) ((IAbortable) inS).abort(); else if (reader != null) reader.close(); } catch (IOException e) { /* Ignore */ } } /* Update News based on uncommitted Items */ Object uncommittedItemsObj = properties.get(IConnectionPropertyConstants.UNCOMMITTED_ITEMS); if (uncommittedItemsObj != null) { Map<String, SyncItem> uncommittedItems = (Map<String, SyncItem>) uncommittedItemsObj; if (!uncommittedItems.isEmpty()) { List<INews> news = feed.getNews(); for (INews item : news) { if (item.getGuid() == null || item.getGuid().getValue() == null) continue; /* Check for Existing Uncommitted SyncItem */ SyncItem syncItem = uncommittedItems.get(item.getGuid().getValue()); if (syncItem == null) continue; /* Apply State from SyncItem to News */ syncItem.applyTo(item); } } } return Triple.create(feed, conditionalGet, link); } private InputStream openGoogleConnection(String authToken, URI googleLink, IProgressMonitor monitor, Map<Object, Object> properties) throws ConnectionException { /* Return on Cancelation or Shutdown */ if (monitor.isCanceled()) return null; /* Fill necessary headers to retrieve feed from Google */ Map<String, String> headers = new HashMap<String, String>(); headers.put(REQUEST_HEADER_AUTHORIZATION, SyncUtils.getGoogleAuthorizationHeader(authToken)); headers.put(REQUEST_HEADER_ACCEPT_CHARSET, UTF_8.toLowerCase()); headers.put(REQUEST_HEADER_USER_AGENT, BROWSER_USER_AGENT); //Necessary as otherwise the content is not sent over as gzip for some unknown reason properties.put(IConnectionPropertyConstants.HEADERS, headers); /* Add Monitor to support early cancelation */ properties.put(IConnectionPropertyConstants.PROGRESS_MONITOR, monitor); return openStream(googleLink, properties); } private String handleAuthentication(boolean refresh, IProgressMonitor monitor) throws ConnectionException { /* Obtain Google Credentials */ URI googleLoginUri = URI.create(SyncUtils.GOOGLE_LOGIN_URL); ICredentialsProvider provider = Owl.getConnectionService().getCredentialsProvider(googleLoginUri); ICredentials credentials = provider.getAuthCredentials(googleLoginUri, null); if (credentials == null) throw new AuthenticationRequiredException(null, Status.CANCEL_STATUS); /* Obtain Google Authentication Token */ String token = SyncUtils.getGoogleAuthToken(credentials.getUsername(), credentials.getPassword(), refresh, monitor); if (token == null) throw new AuthenticationRequiredException(null, Status.CANCEL_STATUS); return token; } /** * Parameters: * <ul> * <li>ot=[unix timestamp] : The time from which you want to retrieve items.</li> * <li>r=[d|n|o] : Sort order of item results.</li> * <li>xt=[exclude target] : Used to exclude certain items from the feed.</li> * <li>n=[integer] : The maximum number of results to return.</li> * <li>ck=[unix timestamp] : Use the current Unix time here, helps Google with * caching.</li> * <li>client=[your client] : You can use the default Google client (scroll).</li> * </ul> */ private URI readerToGoogle(URI uri, int itemLimit, long dateLimit) throws ConnectionException { /* Handle Special Feeds */ String linkVal = uri.toString(); try { /* All Items */ if (SyncUtils.GOOGLE_READER_ALL_ITEMS_FEED.equals(linkVal)) return new URI(appendCommonParams(SyncUtils.GOOGLE_STREAM_CONTENTS_URL + "user/-/state/com.google/reading-list", itemLimit, dateLimit, false)); //$NON-NLS-1$ /* Starred Items */ else if (SyncUtils.GOOGLE_READER_STARRED_FEED.equals(linkVal)) return new URI(appendCommonParams(SyncUtils.GOOGLE_STREAM_CONTENTS_URL + "user/-/state/com.google/starred", itemLimit, dateLimit, false)); //$NON-NLS-1$ /* Shared Items */ else if (SyncUtils.GOOGLE_READER_SHARED_ITEMS_FEED.equals(linkVal)) return new URI(appendCommonParams(SyncUtils.GOOGLE_STREAM_CONTENTS_URL + "user/-/state/com.google/broadcast", itemLimit, dateLimit, false)); //$NON-NLS-1$ /* Recommended Items */ else if (SyncUtils.GOOGLE_READER_RECOMMENDED_ITEMS_FEED.equals(linkVal)) { String language = Locale.getDefault().getLanguage(); return new URI(appendCommonParams(SyncUtils.GOOGLE_STREAM_CONTENTS_URL + "user/-/state/com.google/itemrecs/" + language, itemLimit, dateLimit, true)); //$NON-NLS-1$ } /* Notes */ else if (SyncUtils.GOOGLE_READER_NOTES_FEED.equals(linkVal)) return new URI(appendCommonParams(SyncUtils.GOOGLE_STREAM_CONTENTS_URL + "user/-/state/com.google/created", itemLimit, dateLimit, false)); //$NON-NLS-1$ } catch (URISyntaxException e) { throw new ConnectionException(Activator.getDefault().createErrorStatus(e.getMessage(), e)); } /* Normal Synchronized Feed */ URI httpUri = readerToHTTP(uri); try { return new URI(appendCommonParams(SyncUtils.GOOGLE_FEED_URL + URIUtils.urlEncode(httpUri.toString()), itemLimit, dateLimit, false)); } catch (URISyntaxException e) { throw new ConnectionException(Activator.getDefault().createErrorStatus(e.getMessage(), e)); } } private String appendCommonParams(String link, int itemLimit, long dateLimit, boolean onlyRecommended) { StringBuilder str = new StringBuilder(link); /* Item Limit */ str.append("?n=").append(itemLimit); //$NON-NLS-1$ /* Client */ str.append("&client=scroll"); //$NON-NLS-1$ /* Date Limit */ if (dateLimit > 0) str.append("&ot=").append(dateLimit); //$NON-NLS-1$ /* No comments or likes */ str.append("&likes=false&comments=false"); //$NON-NLS-1$ /* Only Recommended */ if (onlyRecommended) str.append("&xt=user/-/state/com.google/read&xt=user/-/state/com.google/dislike"); //$NON-NLS-1$ /* Caching Time */ str.append("&ck=").append(System.currentTimeMillis()); //$NON-NLS-1$ return str.toString(); } /* * @see * org.rssowl.core.internal.connection.DefaultProtocolHandler#openStream(java * .net.URI, org.eclipse.core.runtime.IProgressMonitor, java.util.Map) */ @Override public InputStream openStream(URI link, IProgressMonitor monitor, Map<Object, Object> properties) throws ConnectionException { return super.openStream(readerToHTTP(link), monitor, properties); } /* * @see * org.rssowl.core.internal.connection.DefaultProtocolHandler#getFeedIcon( * java.net.URI, org.eclipse.core.runtime.IProgressMonitor) */ @Override public byte[] getFeedIcon(URI link, IProgressMonitor monitor) { try { String linkVal = link.toString(); /* Do not try to resolve special Google Reader feed icons */ if (SyncUtils.GOOGLE_READER_ALL_ITEMS_FEED.equals(linkVal)) return null; else if (SyncUtils.GOOGLE_READER_STARRED_FEED.equals(linkVal)) return null; else if (SyncUtils.GOOGLE_READER_SHARED_ITEMS_FEED.equals(linkVal)) return null; else if (SyncUtils.GOOGLE_READER_RECOMMENDED_ITEMS_FEED.equals(linkVal)) return null; else if (SyncUtils.GOOGLE_READER_NOTES_FEED.equals(linkVal)) return null; /* Otherwise proceed loading feed icon through HTTP */ return super.getFeedIcon(readerToHTTP(link), monitor); } catch (ConnectionException e) { return null; } } /* * @see * org.rssowl.core.internal.connection.DefaultProtocolHandler#getLabel(java * .net.URI, org.eclipse.core.runtime.IProgressMonitor) */ @Override public String getLabel(URI link, IProgressMonitor monitor) throws ConnectionException { String linkVal = link.toString(); /* Do not try to resolve special Google Reader feed labels */ if (SyncUtils.GOOGLE_READER_ALL_ITEMS_FEED.equals(linkVal)) return Messages.ReaderProtocolHandler_GR_ALL_ITEMS; else if (SyncUtils.GOOGLE_READER_STARRED_FEED.equals(linkVal)) return Messages.ReaderProtocolHandler_GR_STARRED_ITEMS; else if (SyncUtils.GOOGLE_READER_SHARED_ITEMS_FEED.equals(linkVal)) return Messages.ReaderProtocolHandler_GR_SHARED_ITEMS; else if (SyncUtils.GOOGLE_READER_RECOMMENDED_ITEMS_FEED.equals(linkVal)) return Messages.ReaderProtocolHandler_GR_RECOMMENDED_ITEMS; else if (SyncUtils.GOOGLE_READER_NOTES_FEED.equals(linkVal)) return Messages.ReaderProtocolHandler_GR_NOTES; /* Otherwise proceed loading feed label through HTTP */ return super.getLabel(readerToHTTP(link), monitor); } /* * @see * org.rssowl.core.internal.connection.DefaultProtocolHandler#getFeed(java * .net.URI, org.eclipse.core.runtime.IProgressMonitor) */ @Override public URI getFeed(URI website, IProgressMonitor monitor) throws ConnectionException { return super.getFeed(readerToHTTP(website), monitor); } /** * Do not override default URLStreamHandler of HTTP/HTTPS and therefor return * NULL. * * @see org.rssowl.core.connection.IProtocolHandler#getURLStreamHandler() */ @Override public URLStreamHandlerService getURLStreamHandler() { return null; } private URI readerToHTTP(URI uri) throws ConnectionException { try { String scheme = SyncUtils.READER_HTTPS_SCHEME.equals(uri.getScheme()) ? URIUtils.HTTPS_SCHEME : URIUtils.HTTP_SCHEME; return new URI(scheme, uri.getUserInfo(), uri.getHost(), uri.getPort(), uri.getPath(), uri.getQuery(), uri.getFragment()); } catch (URISyntaxException e) { throw new ConnectionException(Activator.getDefault().createErrorStatus(e.getMessage(), e)); } } }