Merge branch 'entry-filtering'

This commit is contained in:
Athou
2014-11-10 09:20:26 +01:00
15 changed files with 288 additions and 6 deletions

View File

@@ -0,0 +1,113 @@
package com.commafeed.backend.feed;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import lombok.RequiredArgsConstructor;
import org.apache.commons.jexl2.JexlContext;
import org.apache.commons.jexl2.JexlEngine;
import org.apache.commons.jexl2.JexlException;
import org.apache.commons.jexl2.JexlInfo;
import org.apache.commons.jexl2.MapContext;
import org.apache.commons.jexl2.Script;
import org.apache.commons.jexl2.introspection.JexlMethod;
import org.apache.commons.jexl2.introspection.JexlPropertyGet;
import org.apache.commons.jexl2.introspection.Uberspect;
import org.apache.commons.jexl2.introspection.UberspectImpl;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.LogFactory;
import org.jsoup.Jsoup;
import com.commafeed.backend.model.FeedEntry;
@RequiredArgsConstructor
public class FeedEntryFilter {
private static final JexlEngine ENGINE = initEngine();
private static JexlEngine initEngine() {
// classloader that prevents object creation
ClassLoader cl = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
return null;
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
return null;
}
};
// uberspect that prevents access to .class and .getClass()
Uberspect uberspect = new UberspectImpl(LogFactory.getLog(JexlEngine.class)) {
@Override
public JexlPropertyGet getPropertyGet(Object obj, Object identifier, JexlInfo info) {
if ("class".equals(identifier)) {
return null;
}
return super.getPropertyGet(obj, identifier, info);
}
@Override
public JexlMethod getMethod(Object obj, String method, Object[] args, JexlInfo info) {
if ("getClass".equals(method)) {
return null;
}
return super.getMethod(obj, method, args, info);
}
};
JexlEngine engine = new JexlEngine(uberspect, null, null, null);
engine.setStrict(true);
engine.setClassLoader(cl);
return engine;
}
private final String filter;
public boolean matchesEntry(FeedEntry entry) throws FeedEntryFilterException {
if (StringUtils.isBlank(filter)) {
return true;
}
Script script = null;
try {
script = ENGINE.createScript(filter);
} catch (JexlException e) {
throw new FeedEntryFilterException("Exception while parsing expression " + filter, e);
}
JexlContext context = new MapContext();
context.set("title", Jsoup.parse(entry.getContent().getTitle()).text().toLowerCase());
context.set("author", entry.getContent().getAuthor().toLowerCase());
context.set("content", Jsoup.parse(entry.getContent().getContent()).text().toLowerCase());
context.set("url", entry.getUrl().toLowerCase());
Callable<Object> callable = script.callable(context);
Future<Object> future = Executors.newFixedThreadPool(1).submit(callable);
Object result = null;
try {
result = future.get(500, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
throw new FeedEntryFilterException("interrupted while evaluating expression " + filter, e);
} catch (ExecutionException e) {
throw new FeedEntryFilterException("Exception while evaluating expression " + filter, e);
} catch (TimeoutException e) {
throw new FeedEntryFilterException("Took too long evaluating expression " + filter, e);
}
return (boolean) result;
}
@SuppressWarnings("serial")
public static class FeedEntryFilterException extends Exception {
public FeedEntryFilterException(String message, Throwable t) {
super(message, t);
}
}
}

View File

@@ -193,7 +193,7 @@ public class FeedRefreshUpdater implements Managed {
boolean inserted = new UnitOfWork<Boolean>(sessionFactory) {
@Override
protected Boolean runInSession() throws Exception {
return feedUpdateService.addEntry(feed, entry);
return feedUpdateService.addEntry(feed, entry, subscriptions);
}
}.run();
if (inserted) {

View File

@@ -40,4 +40,7 @@ public class FeedSubscription extends AbstractModel {
private Integer position;
@Column(length = 4096)
private String filter;
}

View File

@@ -1,30 +1,39 @@
package com.commafeed.backend.service;
import java.util.Date;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import com.commafeed.backend.dao.FeedEntryDAO;
import com.commafeed.backend.dao.FeedEntryStatusDAO;
import com.commafeed.backend.feed.FeedEntryFilter;
import com.commafeed.backend.feed.FeedEntryFilter.FeedEntryFilterException;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryContent;
import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.backend.model.FeedSubscription;
@Slf4j
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
@Singleton
public class FeedUpdateService {
private final FeedEntryDAO feedEntryDAO;
private final FeedEntryStatusDAO feedEntryStatusDAO;
private final FeedEntryContentService feedEntryContentService;
/**
* this is NOT thread-safe
*/
public boolean addEntry(Feed feed, FeedEntry entry) {
public boolean addEntry(Feed feed, FeedEntry entry, List<FeedSubscription> subscriptions) {
Long existing = feedEntryDAO.findExisting(entry.getGuid(), feed);
if (existing != null) {
@@ -36,8 +45,24 @@ public class FeedUpdateService {
entry.setContent(content);
entry.setInserted(new Date());
entry.setFeed(feed);
feedEntryDAO.saveOrUpdate(entry);
// if filter does not match the entry, mark it as read
for (FeedSubscription sub : subscriptions) {
FeedEntryFilter filter = new FeedEntryFilter(sub.getFilter());
boolean matches = true;
try {
matches = filter.matchesEntry(entry);
} catch (FeedEntryFilterException e) {
log.error("could not evaluate filter {}", sub.getFilter(), e);
}
if (!matches) {
FeedEntryStatus status = new FeedEntryStatus(sub.getUser(), sub, entry);
status.setRead(true);
feedEntryStatusDAO.saveOrUpdate(status);
}
}
return true;
}
}

View File

@@ -35,6 +35,7 @@ public class Subscription implements Serializable {
sub.setUnread(unreadCount.getUnreadCount());
sub.setNewestItemTime(unreadCount.getNewestItemTime());
sub.setCategoryId(category == null ? null : String.valueOf(category.getId()));
sub.setFilter(subscription.getFilter());
return sub;
}
@@ -77,4 +78,7 @@ public class Subscription implements Serializable {
@ApiModelProperty("date of the newest item")
private Date newestItemTime;
@ApiModelProperty(value = "JEXL string evaluated on new entries to mark them as read if they do not match")
private String filter;
}

View File

@@ -24,4 +24,7 @@ public class FeedModificationRequest implements Serializable {
@ApiModelProperty(value = "new display position, null if not changed")
private Integer position;
@ApiModelProperty(value = "JEXL string evaluated on new entries to mark them as read if they do not match")
private String filter;
}

View File

@@ -44,6 +44,8 @@ import com.commafeed.backend.cache.CacheService;
import com.commafeed.backend.dao.FeedCategoryDAO;
import com.commafeed.backend.dao.FeedEntryStatusDAO;
import com.commafeed.backend.dao.FeedSubscriptionDAO;
import com.commafeed.backend.feed.FeedEntryFilter;
import com.commafeed.backend.feed.FeedEntryFilter.FeedEntryFilterException;
import com.commafeed.backend.feed.FeedEntryKeyword;
import com.commafeed.backend.feed.FeedFetcher;
import com.commafeed.backend.feed.FeedQueues;
@@ -51,6 +53,8 @@ import com.commafeed.backend.feed.FeedUtils;
import com.commafeed.backend.feed.FetchedFeed;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryContent;
import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.User;
@@ -94,6 +98,20 @@ import com.wordnik.swagger.annotations.ApiParam;
@Singleton
public class FeedREST {
private static final FeedEntry TEST_ENTRY = initTestEntry();
private static FeedEntry initTestEntry() {
FeedEntry entry = new FeedEntry();
entry.setUrl("https://github.com/Athou/commafeed");
FeedEntryContent content = new FeedEntryContent();
content.setAuthor("Athou");
content.setTitle("Merge pull request #662 from Athou/dw8");
content.setContent("Merge pull request #662 from Athou/dw8");
entry.setContent(content);
return entry;
}
private final FeedSubscriptionDAO feedSubscriptionDAO;
private final FeedCategoryDAO feedCategoryDAO;
private final FeedEntryStatusDAO feedEntryStatusDAO;
@@ -421,7 +439,15 @@ public class FeedREST {
Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getId());
try {
new FeedEntryFilter(req.getFilter()).matchesEntry(TEST_ENTRY);
} catch (FeedEntryFilterException e) {
Throwable root = Throwables.getRootCause(e);
return Response.status(Status.BAD_REQUEST).entity(root.getMessage()).build();
}
FeedSubscription subscription = feedSubscriptionDAO.findById(user, req.getId());
subscription.setFilter(req.getFilter());
if (StringUtils.isNotBlank(req.getName())) {
subscription.setTitle(req.getName());