From 5f28fd4114f61300fc76e71524defa3bd1a13193 Mon Sep 17 00:00:00 2001 From: Athou Date: Tue, 4 Nov 2014 11:23:58 +0100 Subject: [PATCH] initial support for entry filtering --- pom.xml | 6 ++ .../backend/feed/FeedEntryFilter.java | 89 +++++++++++++++++++ .../backend/feed/FeedRefreshUpdater.java | 2 +- .../backend/model/FeedSubscription.java | 3 + .../backend/service/FeedUpdateService.java | 20 ++++- .../resources/changelogs/db.changelog-2.1.xml | 11 +++ src/main/resources/migrations.xml | 1 + .../backend/feed/FeedEntryFilterTest.java | 64 +++++++++++++ 8 files changed, 193 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/commafeed/backend/feed/FeedEntryFilter.java create mode 100644 src/main/resources/changelogs/db.changelog-2.1.xml create mode 100644 src/test/java/com/commafeed/backend/feed/FeedEntryFilterTest.java 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)); + } + +}