diff --git a/pom.xml b/pom.xml
index 43157dd6..29840754 100644
--- a/pom.xml
+++ b/pom.xml
@@ -191,6 +191,12 @@
1.7.7
+
+ org.apache.commons
+ commons-jexl
+ 2.1.1
+
+
com.google.inject
guice
diff --git a/src/main/java/com/commafeed/backend/feed/FeedEntryFilter.java b/src/main/java/com/commafeed/backend/feed/FeedEntryFilter.java
new file mode 100644
index 00000000..03d6fbd7
--- /dev/null
+++ b/src/main/java/com/commafeed/backend/feed/FeedEntryFilter.java
@@ -0,0 +1,89 @@
+package com.commafeed.backend.feed;
+
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+
+import org.apache.commons.jexl2.Expression;
+import org.apache.commons.jexl2.JexlContext;
+import org.apache.commons.jexl2.JexlEngine;
+import org.apache.commons.jexl2.JexlInfo;
+import org.apache.commons.jexl2.MapContext;
+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 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.setClassLoader(cl);
+ return engine;
+ }
+
+ private final String filter;
+
+ public boolean matchesEntry(FeedEntry entry) {
+ if (StringUtils.isBlank(filter)) {
+ return true;
+ }
+
+ Expression expression = ENGINE.createExpression(filter);
+
+ JexlContext context = new MapContext();
+ context.set("title", entry.getContent().getTitle().toLowerCase());
+ context.set("author", entry.getContent().getAuthor().toLowerCase());
+ context.set("content", entry.getContent().getContent().toLowerCase());
+ context.set("url", entry.getUrl().toLowerCase());
+
+ return (boolean) expression.evaluate(context);
+ }
+
+ @Data
+ private static class Model {
+ private String title;
+ private String author;
+ private String content;
+ private String url;
+ }
+
+}
diff --git a/src/main/java/com/commafeed/backend/feed/FeedRefreshUpdater.java b/src/main/java/com/commafeed/backend/feed/FeedRefreshUpdater.java
index b80de7c0..52beb7ca 100644
--- a/src/main/java/com/commafeed/backend/feed/FeedRefreshUpdater.java
+++ b/src/main/java/com/commafeed/backend/feed/FeedRefreshUpdater.java
@@ -193,7 +193,7 @@ public class FeedRefreshUpdater implements Managed {
boolean inserted = new UnitOfWork(sessionFactory) {
@Override
protected Boolean runInSession() throws Exception {
- return feedUpdateService.addEntry(feed, entry);
+ return feedUpdateService.addEntry(feed, entry, subscriptions);
}
}.run();
if (inserted) {
diff --git a/src/main/java/com/commafeed/backend/model/FeedSubscription.java b/src/main/java/com/commafeed/backend/model/FeedSubscription.java
index 363665eb..5ef2e711 100644
--- a/src/main/java/com/commafeed/backend/model/FeedSubscription.java
+++ b/src/main/java/com/commafeed/backend/model/FeedSubscription.java
@@ -40,4 +40,7 @@ public class FeedSubscription extends AbstractModel {
private Integer position;
+ @Column(length = 4096)
+ private String filter = "author.contains('a')";
+
}
diff --git a/src/main/java/com/commafeed/backend/service/FeedUpdateService.java b/src/main/java/com/commafeed/backend/service/FeedUpdateService.java
index e0c68e17..0f51651e 100644
--- a/src/main/java/com/commafeed/backend/service/FeedUpdateService.java
+++ b/src/main/java/com/commafeed/backend/service/FeedUpdateService.java
@@ -1,6 +1,7 @@
package com.commafeed.backend.service;
import java.util.Date;
+import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
@@ -10,21 +11,26 @@ import lombok.RequiredArgsConstructor;
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.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;
@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 subscriptions) {
Long existing = feedEntryDAO.findExisting(entry.getGuid(), feed);
if (existing != null) {
@@ -36,8 +42,18 @@ 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());
+ if (!filter.matchesEntry(entry)) {
+ FeedEntryStatus status = new FeedEntryStatus(sub.getUser(), sub, entry);
+ status.setRead(true);
+ feedEntryStatusDAO.saveOrUpdate(status);
+ }
+ }
+
return true;
}
}
diff --git a/src/main/resources/changelogs/db.changelog-2.1.xml b/src/main/resources/changelogs/db.changelog-2.1.xml
new file mode 100644
index 00000000..df158560
--- /dev/null
+++ b/src/main/resources/changelogs/db.changelog-2.1.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/migrations.xml b/src/main/resources/migrations.xml
index acc1dafa..8da3a73d 100644
--- a/src/main/resources/migrations.xml
+++ b/src/main/resources/migrations.xml
@@ -9,5 +9,6 @@
+
\ No newline at end of file
diff --git a/src/test/java/com/commafeed/backend/feed/FeedEntryFilterTest.java b/src/test/java/com/commafeed/backend/feed/FeedEntryFilterTest.java
new file mode 100644
index 00000000..679dc63e
--- /dev/null
+++ b/src/test/java/com/commafeed/backend/feed/FeedEntryFilterTest.java
@@ -0,0 +1,64 @@
+package com.commafeed.backend.feed;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.commafeed.backend.model.FeedEntry;
+import com.commafeed.backend.model.FeedEntryContent;
+
+public class FeedEntryFilterTest {
+
+ private FeedEntry entry;
+ private FeedEntryContent content;
+
+ @Before
+ public void init() {
+ entry = new FeedEntry();
+ entry.setUrl("https://github.com/Athou/commafeed");
+
+ 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);
+
+ }
+
+ @Test
+ public void emptyFilterMatchesFilter() {
+ FeedEntryFilter filter = new FeedEntryFilter(null);
+ Assert.assertTrue(filter.matchesEntry(entry));
+ }
+
+ @Test
+ public void blankFilterMatchesFilter() {
+ FeedEntryFilter filter = new FeedEntryFilter("");
+ Assert.assertTrue(filter.matchesEntry(entry));
+ }
+
+ @Test
+ public void simpleExpression() {
+ FeedEntryFilter filter = new FeedEntryFilter("author eq 'athou'");
+ Assert.assertTrue(filter.matchesEntry(entry));
+ }
+
+ @Test
+ public void newIsDisabled() {
+ FeedEntryFilter filter = new FeedEntryFilter("null eq new ('java.lang.String', 'athou')");
+ Assert.assertTrue(filter.matchesEntry(entry));
+ }
+
+ @Test
+ public void getClassMethodIsDisabled() {
+ FeedEntryFilter filter = new FeedEntryFilter("null eq ''.getClass()");
+ Assert.assertTrue(filter.matchesEntry(entry));
+ }
+
+ @Test
+ public void dotClassIsDisabled() {
+ FeedEntryFilter filter = new FeedEntryFilter("null eq ''.class");
+ Assert.assertTrue(filter.matchesEntry(entry));
+ }
+
+}