forked from Archives/Athou_commafeed
initial support for entry filtering
This commit is contained in:
6
pom.xml
6
pom.xml
@@ -191,6 +191,12 @@
|
|||||||
<version>1.7.7</version>
|
<version>1.7.7</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.commons</groupId>
|
||||||
|
<artifactId>commons-jexl</artifactId>
|
||||||
|
<version>2.1.1</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.google.inject</groupId>
|
<groupId>com.google.inject</groupId>
|
||||||
<artifactId>guice</artifactId>
|
<artifactId>guice</artifactId>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -193,7 +193,7 @@ public class FeedRefreshUpdater implements Managed {
|
|||||||
boolean inserted = new UnitOfWork<Boolean>(sessionFactory) {
|
boolean inserted = new UnitOfWork<Boolean>(sessionFactory) {
|
||||||
@Override
|
@Override
|
||||||
protected Boolean runInSession() throws Exception {
|
protected Boolean runInSession() throws Exception {
|
||||||
return feedUpdateService.addEntry(feed, entry);
|
return feedUpdateService.addEntry(feed, entry, subscriptions);
|
||||||
}
|
}
|
||||||
}.run();
|
}.run();
|
||||||
if (inserted) {
|
if (inserted) {
|
||||||
|
|||||||
@@ -40,4 +40,7 @@ public class FeedSubscription extends AbstractModel {
|
|||||||
|
|
||||||
private Integer position;
|
private Integer position;
|
||||||
|
|
||||||
|
@Column(length = 4096)
|
||||||
|
private String filter = "author.contains('a')";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.commafeed.backend.service;
|
package com.commafeed.backend.service;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
@@ -10,21 +11,26 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import org.apache.commons.codec.digest.DigestUtils;
|
import org.apache.commons.codec.digest.DigestUtils;
|
||||||
|
|
||||||
import com.commafeed.backend.dao.FeedEntryDAO;
|
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.Feed;
|
||||||
import com.commafeed.backend.model.FeedEntry;
|
import com.commafeed.backend.model.FeedEntry;
|
||||||
import com.commafeed.backend.model.FeedEntryContent;
|
import com.commafeed.backend.model.FeedEntryContent;
|
||||||
|
import com.commafeed.backend.model.FeedEntryStatus;
|
||||||
|
import com.commafeed.backend.model.FeedSubscription;
|
||||||
|
|
||||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||||
@Singleton
|
@Singleton
|
||||||
public class FeedUpdateService {
|
public class FeedUpdateService {
|
||||||
|
|
||||||
private final FeedEntryDAO feedEntryDAO;
|
private final FeedEntryDAO feedEntryDAO;
|
||||||
|
private final FeedEntryStatusDAO feedEntryStatusDAO;
|
||||||
private final FeedEntryContentService feedEntryContentService;
|
private final FeedEntryContentService feedEntryContentService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* this is NOT thread-safe
|
* 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);
|
Long existing = feedEntryDAO.findExisting(entry.getGuid(), feed);
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
@@ -36,8 +42,18 @@ public class FeedUpdateService {
|
|||||||
entry.setContent(content);
|
entry.setContent(content);
|
||||||
entry.setInserted(new Date());
|
entry.setInserted(new Date());
|
||||||
entry.setFeed(feed);
|
entry.setFeed(feed);
|
||||||
|
|
||||||
feedEntryDAO.saveOrUpdate(entry);
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/main/resources/changelogs/db.changelog-2.1.xml
Normal file
11
src/main/resources/changelogs/db.changelog-2.1.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
|
||||||
|
|
||||||
|
<changeSet id="add-sub-filter" author="athou">
|
||||||
|
<addColumn tableName="FEEDSUBSCRIPTIONS">
|
||||||
|
<column name="filter" type="varchar(4096)" />
|
||||||
|
</addColumn>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
@@ -9,5 +9,6 @@
|
|||||||
<include file="changelogs/db.changelog-1.3.xml" />
|
<include file="changelogs/db.changelog-1.3.xml" />
|
||||||
<include file="changelogs/db.changelog-1.4.xml" />
|
<include file="changelogs/db.changelog-1.4.xml" />
|
||||||
<include file="changelogs/db.changelog-1.5.xml" />
|
<include file="changelogs/db.changelog-1.5.xml" />
|
||||||
|
<include file="changelogs/db.changelog-2.1.xml" />
|
||||||
|
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user