Implementing Optional GET with If-None-Match and If-Modified-Since headers

This commit is contained in:
Shinokuni 2019-02-06 21:01:51 +00:00
parent f384d5b9c8
commit 098ae50044
7 changed files with 145 additions and 41 deletions

View File

@ -15,6 +15,7 @@ import com.readrops.app.database.entities.Item;
import com.readrops.readropslibrary.HtmlParser; import com.readrops.readropslibrary.HtmlParser;
import com.readrops.readropslibrary.ParsingResult; import com.readrops.readropslibrary.ParsingResult;
import com.readrops.readropslibrary.QueryCallback; import com.readrops.readropslibrary.QueryCallback;
import com.readrops.readropslibrary.Utils.LibUtils;
import com.readrops.readropslibrary.localfeed.AFeed; import com.readrops.readropslibrary.localfeed.AFeed;
import com.readrops.readropslibrary.localfeed.RSSNetwork; import com.readrops.readropslibrary.localfeed.RSSNetwork;
import com.readrops.readropslibrary.localfeed.atom.ATOMFeed; import com.readrops.readropslibrary.localfeed.atom.ATOMFeed;
@ -25,6 +26,7 @@ import org.jsoup.Jsoup;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.HashMap;
import java.util.List; import java.util.List;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
@ -51,11 +53,18 @@ public class LocalFeedRepository extends ARepository implements QueryCallback {
public void sync() { public void sync() {
executor.execute(() -> { executor.execute(() -> {
RSSNetwork rssNet = new RSSNetwork(); RSSNetwork rssNet = new RSSNetwork();
rssNet.setCallback(this);
List<Feed> feedList = database.feedDao().getAllFeeds(); List<Feed> feedList = database.feedDao().getAllFeeds();
for (Feed feed : feedList) { for (Feed feed : feedList) {
try { try {
rssNet.request(feed.getUrl(), this); HashMap<String, String> headers = new HashMap<>();
if (feed.getEtag() != null)
headers.put(LibUtils.IF_NONE_MATCH_HEADER, feed.getEtag());
if (feed.getLastModified() != null)
headers.put(LibUtils.IF_MODIFIED_HEADER, feed.getLastModified());
rssNet.request(feed.getUrl(), headers);
} catch (Exception e) { } catch (Exception e) {
failureCallBackInMainThread(e); failureCallBackInMainThread(e);
} }
@ -70,7 +79,8 @@ public class LocalFeedRepository extends ARepository implements QueryCallback {
executor.execute(() -> { executor.execute(() -> {
try { try {
RSSNetwork rssNet = new RSSNetwork(); RSSNetwork rssNet = new RSSNetwork();
rssNet.request(result.getUrl(), this); rssNet.setCallback(this);
rssNet.request(result.getUrl(), new HashMap<>());
postCallBackSuccess(); postCallBackSuccess();
} catch (Exception e) { } catch (Exception e) {
@ -116,11 +126,12 @@ public class LocalFeedRepository extends ARepository implements QueryCallback {
try { try {
Feed dbFeed = database.feedDao().getFeedByUrl(rssFeed.getChannel().getFeedUrl()); Feed dbFeed = database.feedDao().getFeedByUrl(rssFeed.getChannel().getFeedUrl());
if (dbFeed == null) { if (dbFeed == null) {
dbFeed = Feed.feedFromRSS(rssFeed.getChannel()); dbFeed = Feed.feedFromRSS(rssFeed);
setFavIconUtils(dbFeed); setFavIconUtils(dbFeed);
dbFeed.setId((int)(database.feedDao().insert(dbFeed))); dbFeed.setId((int)(database.feedDao().insert(dbFeed)));
} } else
database.feedDao().updateHeaders(rssFeed.getEtag(), rssFeed.getLastModified(), dbFeed.getId());
List<Item> dbItems = Item.itemsFromRSS(rssFeed.getChannel().getItems(), dbFeed); List<Item> dbItems = Item.itemsFromRSS(rssFeed.getChannel().getItems(), dbFeed);
insertItems(dbItems, dbFeed); insertItems(dbItems, dbFeed);
@ -139,7 +150,8 @@ public class LocalFeedRepository extends ARepository implements QueryCallback {
setFavIconUtils(dbFeed); setFavIconUtils(dbFeed);
dbFeed.setId((int)(database.feedDao().insert(dbFeed))); dbFeed.setId((int)(database.feedDao().insert(dbFeed)));
} } else
database.feedDao().updateHeaders(feed.getEtag(), feed.getLastModified(), dbFeed.getId());
List<Item> dbItems = Item.itemsFromATOM(feed.getEntries(), dbFeed); List<Item> dbItems = Item.itemsFromATOM(feed.getEntries(), dbFeed);
insertItems(dbItems, dbFeed); insertItems(dbItems, dbFeed);
@ -157,7 +169,8 @@ public class LocalFeedRepository extends ARepository implements QueryCallback {
setFavIconUtils(dbFeed); setFavIconUtils(dbFeed);
dbFeed.setId((int)(database.feedDao().insert(dbFeed))); dbFeed.setId((int)(database.feedDao().insert(dbFeed)));
} } else
database.feedDao().updateHeaders(feed.getEtag(), feed.getLastModified(), dbFeed.getId());
List<Item> dbItems = Item.itemsFromJSON(feed.getItems(), dbFeed); List<Item> dbItems = Item.itemsFromJSON(feed.getItems(), dbFeed);
insertItems(dbItems, dbFeed); insertItems(dbItems, dbFeed);

View File

@ -4,6 +4,7 @@ package com.readrops.app.database.dao;
import android.arch.persistence.room.Dao; import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Insert; import android.arch.persistence.room.Insert;
import android.arch.persistence.room.Query; import android.arch.persistence.room.Query;
import android.arch.persistence.room.Update;
import com.readrops.app.database.entities.Feed; import com.readrops.app.database.entities.Feed;
@ -27,4 +28,7 @@ public interface FeedDao {
@Query("Select id from Feed Where url = :feedUrl") @Query("Select id from Feed Where url = :feedUrl")
int getFeedIdByUrl(String feedUrl); int getFeedIdByUrl(String feedUrl);
@Query("Update Feed set etag = :etag, last_modified = :lastModified Where id = :feedId")
void updateHeaders(String etag, String lastModified, int feedId);
} }

View File

@ -7,6 +7,9 @@ import android.support.annotation.ColorInt;
import com.readrops.readropslibrary.localfeed.atom.ATOMFeed; import com.readrops.readropslibrary.localfeed.atom.ATOMFeed;
import com.readrops.readropslibrary.localfeed.json.JSONFeed; import com.readrops.readropslibrary.localfeed.json.JSONFeed;
import com.readrops.readropslibrary.localfeed.rss.RSSChannel; import com.readrops.readropslibrary.localfeed.rss.RSSChannel;
import com.readrops.readropslibrary.localfeed.rss.RSSFeed;
import java.nio.channels.Channel;
@Entity @Entity
public class Feed { public class Feed {
@ -29,6 +32,11 @@ public class Feed {
@ColumnInfo(name = "icon_url") @ColumnInfo(name = "icon_url")
private String iconUrl; private String iconUrl;
private String etag;
@ColumnInfo(name = "last_modified")
private String lastModified;
public Feed() { public Feed() {
} }
@ -104,8 +112,25 @@ public class Feed {
this.iconUrl = iconUrl; this.iconUrl = iconUrl;
} }
public static Feed feedFromRSS(RSSChannel channel) { public String getEtag() {
return etag;
}
public void setEtag(String etag) {
this.etag = etag;
}
public String getLastModified() {
return lastModified;
}
public void setLastModified(String lastModified) {
this.lastModified = lastModified;
}
public static Feed feedFromRSS(RSSFeed rssFeed) {
Feed feed = new Feed(); Feed feed = new Feed();
RSSChannel channel = rssFeed.getChannel();
feed.setName(channel.getTitle()); feed.setName(channel.getTitle());
feed.setUrl(channel.getFeedUrl()); feed.setUrl(channel.getFeedUrl());
@ -113,6 +138,9 @@ public class Feed {
feed.setDescription(channel.getDescription()); feed.setDescription(channel.getDescription());
feed.setLastUpdated(channel.getLastUpdated()); feed.setLastUpdated(channel.getLastUpdated());
feed.setEtag(rssFeed.getEtag());
feed.setLastModified(rssFeed.getLastModified());
return feed; return feed;
} }
@ -126,6 +154,9 @@ public class Feed {
feed.setDescription(atomFeed.getSubtitle()); feed.setDescription(atomFeed.getSubtitle());
feed.setLastUpdated(atomFeed.getUpdated()); feed.setLastUpdated(atomFeed.getUpdated());
feed.setEtag(atomFeed.getEtag());
feed.setLastModified(atomFeed.getLastModified());
return feed; return feed;
} }
@ -137,6 +168,9 @@ public class Feed {
feed.setDescription(jsonFeed.getDescription()); feed.setDescription(jsonFeed.getDescription());
//feed.setLastUpdated(jsonFeed.); maybe later ? //feed.setLastUpdated(jsonFeed.); maybe later ?
feed.setEtag(jsonFeed.getEtag());
feed.setLastModified(jsonFeed.getLastModified());
return feed; return feed;
} }
} }

View File

@ -2,7 +2,7 @@ package com.readrops.readropslibrary;
import android.util.Log; import android.util.Log;
import com.readrops.readropslibrary.Utils.Utils; import com.readrops.readropslibrary.Utils.LibUtils;
import org.jsoup.Connection; import org.jsoup.Connection;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
@ -45,11 +45,11 @@ public final class HtmlParser {
} }
private static boolean isTypeRssFeed(String type) { private static boolean isTypeRssFeed(String type) {
return type.equals(Utils.RSS_DEFAULT_CONTENT_TYPE) || return type.equals(LibUtils.RSS_DEFAULT_CONTENT_TYPE) ||
type.equals(Utils.ATOM_CONTENT_TYPE) || type.equals(LibUtils.ATOM_CONTENT_TYPE) ||
type.equals(Utils.JSON_CONTENT_TYPE) || type.equals(LibUtils.JSON_CONTENT_TYPE) ||
type.equals(Utils.RSS_TEXT_CONTENT_TYPE) || type.equals(LibUtils.RSS_TEXT_CONTENT_TYPE) ||
type.equals(Utils.RSS_APPLICATION_CONTENT_TYPE); type.equals(LibUtils.RSS_APPLICATION_CONTENT_TYPE);
} }
/** /**

View File

@ -3,7 +3,7 @@ package com.readrops.readropslibrary.Utils;
import java.io.InputStream; import java.io.InputStream;
import java.util.Scanner; import java.util.Scanner;
public final class Utils { public final class LibUtils {
public static final String RSS_DEFAULT_CONTENT_TYPE = "application/rss+xml"; public static final String RSS_DEFAULT_CONTENT_TYPE = "application/rss+xml";
public static final String RSS_TEXT_CONTENT_TYPE = "text/xml"; public static final String RSS_TEXT_CONTENT_TYPE = "text/xml";
@ -12,6 +12,12 @@ public final class Utils {
public static final String JSON_CONTENT_TYPE = "application/json"; public static final String JSON_CONTENT_TYPE = "application/json";
public static final String HTML_CONTENT_TYPE = "text/html"; public static final String HTML_CONTENT_TYPE = "text/html";
public static final String CONTENT_TYPE_HEADER = "content-type";
public static final String ETAG_HEADER = "ETag";
public static final String IF_NONE_MATCH_HEADER = "If-None-Match";
public static final String LAST_MODIFIED_HEADER = "Last-Modified";
public static final String IF_MODIFIED_HEADER = "If-Modified-Since";
public static String inputStreamToString(InputStream input) { public static String inputStreamToString(InputStream input) {
Scanner scanner = new Scanner(input).useDelimiter("\\A"); Scanner scanner = new Scanner(input).useDelimiter("\\A");
return scanner.hasNext() ? scanner.next() : ""; return scanner.hasNext() ? scanner.next() : "";

View File

@ -4,4 +4,24 @@ package com.readrops.readropslibrary.localfeed;
A simple class to give an abstract level to rss/atom/json feed classes A simple class to give an abstract level to rss/atom/json feed classes
*/ */
public abstract class AFeed { public abstract class AFeed {
protected String etag;
protected String lastModified;
public String getEtag() {
return etag;
}
public void setEtag(String etag) {
this.etag = etag;
}
public String getLastModified() {
return lastModified;
}
public void setLastModified(String lastModified) {
this.lastModified = lastModified;
}
} }

View File

@ -4,7 +4,7 @@ import android.util.Log;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.readrops.readropslibrary.QueryCallback; import com.readrops.readropslibrary.QueryCallback;
import com.readrops.readropslibrary.Utils.Utils; import com.readrops.readropslibrary.Utils.LibUtils;
import com.readrops.readropslibrary.localfeed.atom.ATOMFeed; import com.readrops.readropslibrary.localfeed.atom.ATOMFeed;
import com.readrops.readropslibrary.localfeed.json.JSONFeed; import com.readrops.readropslibrary.localfeed.json.JSONFeed;
import com.readrops.readropslibrary.localfeed.rss.RSSFeed; import com.readrops.readropslibrary.localfeed.rss.RSSFeed;
@ -14,6 +14,7 @@ import org.simpleframework.xml.Serializer;
import org.simpleframework.xml.core.Persister; import org.simpleframework.xml.core.Persister;
import java.io.InputStream; import java.io.InputStream;
import java.util.Map;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -29,37 +30,48 @@ public class RSSNetwork {
private static final String RSS_2_REGEX = "rss.*version=\"2.0\""; private static final String RSS_2_REGEX = "rss.*version=\"2.0\"";
private QueryCallback callback;
/** /**
* Request the url given in parameter. * Request the url given in parameter.
* This method is synchronous, <b>it has to be called from another thread than the main one</b>. * This method is synchronous, <b>it has to be called from another thread than the main one</b>.
* @param url url to request * @param url url to request
* @param callback result callback if success or error
* @throws Exception * @throws Exception
*/ */
public void request(String url, final QueryCallback callback) throws Exception { public void request(String url, Map<String, String> headers) throws Exception {
if (callback == null)
throw new NullPointerException("Callback can't be null");
OkHttpClient okHttpClient = new OkHttpClient(); OkHttpClient okHttpClient = new OkHttpClient();
Request request = new Request.Builder().url(url).build(); Request.Builder builder = new Request.Builder().url(url);
for (String header : headers.keySet()) {
String value = headers.get(header);
builder.addHeader(header, value);
}
Request request = builder.build();
Response response = okHttpClient.newCall(request).execute(); Response response = okHttpClient.newCall(request).execute();
Pattern pattern = Pattern.compile(RSS_CONTENT_TYPE_REGEX);
Matcher matcher = pattern.matcher(response.header("content-type"));
String header;
if (matcher.find())
header = matcher.group(0);
else
header = response.header("content-type");
if (response.isSuccessful()) { if (response.isSuccessful()) {
Pattern pattern = Pattern.compile(RSS_CONTENT_TYPE_REGEX);
Matcher matcher = pattern.matcher(response.header(LibUtils.CONTENT_TYPE_HEADER));
String header;
if (matcher.find())
header = matcher.group(0);
else
header = response.header(LibUtils.CONTENT_TYPE_HEADER);
RSSType type = getRSSType(header); RSSType type = getRSSType(header);
if (type == null) { if (type == null) {
callback.onSyncFailure(new IllegalArgumentException("bad content type : " + header + "for " + url)); callback.onSyncFailure(new IllegalArgumentException("bad content type : " + header + "for " + url));
return; return;
} }
parseFeed(response.body().byteStream(), type, response);
parseFeed(response.body().byteStream(), type, callback, url); } else if (response.code() == 304)
} else return;
else
callback.onSyncFailure(new Exception("Error " + response.code() + " when requesting url " + url)); callback.onSyncFailure(new Exception("Error " + response.code() + " when requesting url " + url));
} }
@ -67,12 +79,11 @@ public class RSSNetwork {
* Parse input feed * Parse input feed
* @param stream inputStream to parse * @param stream inputStream to parse
* @param type rss type, important to know the format * @param type rss type, important to know the format
* @param callback success callback * @param response query response
* @param url feed url
* @throws Exception * @throws Exception
*/ */
private void parseFeed(InputStream stream, RSSType type, QueryCallback callback, String url) throws Exception { private void parseFeed(InputStream stream, RSSType type, Response response) throws Exception {
String xml = Utils.inputStreamToString(stream); String xml = LibUtils.inputStreamToString(stream);
Serializer serializer = new Persister(); Serializer serializer = new Persister();
if (type == RSSType.RSS_UNKNOWN) { if (type == RSSType.RSS_UNKNOWN) {
@ -86,21 +97,33 @@ public class RSSNetwork {
} }
} }
String etag = response.header(LibUtils.ETAG_HEADER);
String lastModified = response.header(LibUtils.LAST_MODIFIED_HEADER);
switch (type) { switch (type) {
case RSS_2: case RSS_2:
RSSFeed rssFeed = serializer.read(RSSFeed.class, xml); RSSFeed rssFeed = serializer.read(RSSFeed.class, xml);
if (rssFeed.getChannel().getFeedUrl() == null) // workaround si the channel does not have any atom:link tag if (rssFeed.getChannel().getFeedUrl() == null) // workaround si the channel does not have any atom:link tag
rssFeed.getChannel().getLinks().add(new RSSLink(null, url)); rssFeed.getChannel().getLinks().add(new RSSLink(null, response.request().url().toString()));
rssFeed.setEtag(etag);
rssFeed.setLastModified(lastModified);
callback.onSyncSuccess(rssFeed, type); callback.onSyncSuccess(rssFeed, type);
break; break;
case RSS_ATOM: case RSS_ATOM:
ATOMFeed atomFeed = serializer.read(ATOMFeed.class, xml); ATOMFeed atomFeed = serializer.read(ATOMFeed.class, xml);
atomFeed.setEtag(etag);
atomFeed.setLastModified(etag);
callback.onSyncSuccess(atomFeed, type); callback.onSyncSuccess(atomFeed, type);
break; break;
case RSS_JSON: case RSS_JSON:
Gson gson = new Gson(); Gson gson = new Gson();
JSONFeed feed = gson.fromJson(xml, JSONFeed.class); JSONFeed feed = gson.fromJson(xml, JSONFeed.class);
feed.setEtag(etag);
feed.setLastModified(lastModified);
callback.onSyncSuccess(feed, type); callback.onSyncSuccess(feed, type);
break; break;
} }
@ -113,17 +136,17 @@ public class RSSNetwork {
*/ */
private RSSType getRSSType(String contentType) { private RSSType getRSSType(String contentType) {
switch (contentType) { switch (contentType) {
case Utils.RSS_DEFAULT_CONTENT_TYPE: case LibUtils.RSS_DEFAULT_CONTENT_TYPE:
return RSSType.RSS_2; return RSSType.RSS_2;
case Utils.RSS_TEXT_CONTENT_TYPE: case LibUtils.RSS_TEXT_CONTENT_TYPE:
return RSSType.RSS_UNKNOWN; return RSSType.RSS_UNKNOWN;
case Utils.RSS_APPLICATION_CONTENT_TYPE: case LibUtils.RSS_APPLICATION_CONTENT_TYPE:
return RSSType.RSS_UNKNOWN; return RSSType.RSS_UNKNOWN;
case Utils.ATOM_CONTENT_TYPE: case LibUtils.ATOM_CONTENT_TYPE:
return RSSType.RSS_ATOM; return RSSType.RSS_ATOM;
case Utils.JSON_CONTENT_TYPE: case LibUtils.JSON_CONTENT_TYPE:
return RSSType.RSS_JSON; return RSSType.RSS_JSON;
case Utils.HTML_CONTENT_TYPE: case LibUtils.HTML_CONTENT_TYPE:
return RSSType.RSS_UNKNOWN; return RSSType.RSS_UNKNOWN;
default: default:
Log.d(TAG, "bad content type : " + contentType); Log.d(TAG, "bad content type : " + contentType);
@ -132,6 +155,10 @@ public class RSSNetwork {
} }
} }
public void setCallback(QueryCallback callback) {
this.callback = callback;
}
public enum RSSType { public enum RSSType {
RSS_2("rss"), RSS_2("rss"),
RSS_ATOM("atom"), RSS_ATOM("atom"),