search for entries

This commit is contained in:
Athou
2013-04-03 13:33:33 +02:00
parent 4c25bcdc1b
commit ed189c1c31
10 changed files with 135 additions and 36 deletions

View File

@@ -65,6 +65,29 @@ public class FeedEntryService extends GenericDAO<FeedEntry> {
return query.getResultList(); return query.getResultList();
} }
public List<FeedEntryWithStatus> getEntriesByKeywords(User user,
String keywords) {
return getEntriesByKeywords(user, keywords, -1, -1);
}
public List<FeedEntryWithStatus> getEntriesByKeywords(User user,
String keywords, int offset, int limit) {
Query query = em.createNamedQuery("Entry.allByKeywords");
query.setParameter("userId", user.getId());
query.setParameter("user", user);
String joinedKeywords = StringUtils.join(
keywords.toLowerCase().split(" "), "%");
query.setParameter("keywords", "%" + joinedKeywords + "%");
if (offset > -1) {
query.setFirstResult(offset);
}
if (limit > -1) {
query.setMaxResults(limit);
}
return buildList(query.getResultList());
}
public List<FeedEntryWithStatus> getEntries(User user, boolean unreadOnly) { public List<FeedEntryWithStatus> getEntries(User user, boolean unreadOnly) {
return getEntries(user, unreadOnly, -1, -1); return getEntries(user, unreadOnly, -1, -1);
} }

View File

@@ -13,7 +13,6 @@ import javax.persistence.Table;
import javax.persistence.Temporal; import javax.persistence.Temporal;
import javax.persistence.TemporalType; import javax.persistence.TemporalType;
import org.apache.commons.codec.binary.StringUtils;
import org.hibernate.annotations.Index; import org.hibernate.annotations.Index;
@Entity @Entity
@@ -32,7 +31,8 @@ public class FeedEntry extends AbstractModel {
private String title; private String title;
@Lob @Lob
private byte[] content; @Column(length = Integer.MAX_VALUE)
private String content;
@Column(length = 2048) @Column(length = 2048)
private String url; private String url;
@@ -61,11 +61,11 @@ public class FeedEntry extends AbstractModel {
} }
public String getContent() { public String getContent() {
return StringUtils.newStringUtf8(content); return content;
} }
public void setContent(String content) { public void setContent(String content) {
this.content = StringUtils.getBytesUtf8(content); this.content = content;
} }
public String getUrl() { public String getUrl() {

View File

@@ -10,6 +10,8 @@ import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.Response.Status;
import org.apache.commons.lang.StringUtils;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedCategory; import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntry;
@@ -223,4 +225,33 @@ public class EntriesREST extends AbstractREST {
feedEntryStatusService.saveOrUpdate(status); feedEntryStatusService.saveOrUpdate(status);
} }
@Path("search")
@GET
public Entries searchEntries(@QueryParam("keywords") String keywords) {
Preconditions.checkArgument(StringUtils.length(keywords) >= 3);
Entries entries = new Entries();
List<FeedSubscription> subs = feedSubscriptionService
.findAll(getUser());
Map<Long, FeedSubscription> subMapping = Maps.uniqueIndex(subs,
new Function<FeedSubscription, Long>() {
public Long apply(FeedSubscription sub) {
return sub.getFeed().getId();
}
});
List<Entry> list = Lists.newArrayList();
List<FeedEntryWithStatus> entriesWithStatus = feedEntryService
.getEntriesByKeywords(getUser(), keywords);
for (FeedEntryWithStatus feedEntry : entriesWithStatus) {
Long id = feedEntry.getEntry().getFeed().getId();
list.add(populateEntry(buildEntry(feedEntry), subMapping.get(id)));
}
entries.setName("Search for : " + keywords);
entries.getEntries().addAll(list);
return entries;
}
} }

View File

@@ -29,4 +29,8 @@
<named-query name="Entry.allByCategories"> <named-query name="Entry.allByCategories">
<query>select e, s from FeedEntry e LEFT JOIN e.statuses s WITH (s.user.id=:userId) where exists (select s2 from FeedSubscription s2 where s2.user=:user and s2.feed = e.feed and s2.category in (:categories) ) order by e.updated desc</query> <query>select e, s from FeedEntry e LEFT JOIN e.statuses s WITH (s.user.id=:userId) where exists (select s2 from FeedSubscription s2 where s2.user=:user and s2.feed = e.feed and s2.category in (:categories) ) order by e.updated desc</query>
</named-query> </named-query>
<named-query name="Entry.allByKeywords">
<query>select e, s from FeedEntry e LEFT JOIN e.statuses s WITH (s.user.id=:userId) where exists (select s2 from FeedSubscription s2 where s2.user=:user and s2.feed = e.feed) and lower(e.content) like :keywords order by e.updated desc</query>
</named-query>
</entity-mappings> </entity-mappings>

View File

@@ -1,6 +1,6 @@
<div> <div>
<div class="form-horizontal">
<div> <span ui-if="showButtons()">
<div class="btn-group read-mode" data-toggle="buttons-radio"> <div class="btn-group read-mode" data-toggle="buttons-radio">
<button type="button" class="btn" ng-model="settingsService.settings.readingMode" btn-radio="'unread'">Unread</button> <button type="button" class="btn" ng-model="settingsService.settings.readingMode" btn-radio="'unread'">Unread</button>
<button type="button" class="btn" ng-model="settingsService.settings.readingMode" btn-radio="'all'">All</button> <button type="button" class="btn" ng-model="settingsService.settings.readingMode" btn-radio="'all'">All</button>
@@ -18,7 +18,11 @@
<li><a href="logout"><i class="icon-user"></i> Logout</a></li> <li><a href="logout"><i class="icon-user"></i> Logout</a></li>
</ul> </ul>
</div> </div>
</span>
<form ng-submit="search()" class="input-append">
<input type="text" ng-model="keywords"></input>
<button class="btn" type="submit"><i class="icon-search"></i></button>
</form>
<div spinner shown="loading"></div> <div spinner shown="loading"></div>
</div> </div>
</div> </div>

View File

@@ -110,6 +110,7 @@ module.controller('FeedListCtrl', function($scope, $stateParams, $http, $route,
$scope.selectedType = $stateParams._type; $scope.selectedType = $stateParams._type;
$scope.selectedId = $stateParams._id; $scope.selectedId = $stateParams._id;
$scope.keywords = $stateParams._keywords;
$scope.name = null; $scope.name = null;
$scope.message = null; $scope.message = null;
@@ -140,13 +141,8 @@ module.controller('FeedListCtrl', function($scope, $stateParams, $http, $route,
limit = $window.height() / 33; limit = $window.height() / 33;
limit = parseInt(limit, 10) + 5; limit = parseInt(limit, 10) + 5;
} }
EntryService.get({
type : $scope.selectedType, var callback = function(data) {
id : $scope.selectedId,
readType : $scope.settingsService.settings.readingMode,
offset : $scope.entries.length,
limit : limit
}, function(data) {
for ( var i = 0; i < data.entries.length; i++) { for ( var i = 0; i < data.entries.length; i++) {
$scope.entries.push(data.entries[i]); $scope.entries.push(data.entries[i]);
} }
@@ -154,7 +150,22 @@ module.controller('FeedListCtrl', function($scope, $stateParams, $http, $route,
$scope.message = data.message; $scope.message = data.message;
$scope.busy = false; $scope.busy = false;
$scope.hasMore = data.entries.length == limit; $scope.hasMore = data.entries.length == limit;
}); };
if (!$scope.keywords) {
EntryService.get({
type : $scope.selectedType,
id : $scope.selectedId,
readType : $scope.settingsService.settings.readingMode,
offset : $scope.entries.length,
limit : limit
}, callback);
} else {
EntryService.search({
keywords : $scope.keywords,
offset : $scope.entries.length,
limit : limit
}, callback);
}
}; };
$scope.mark = function(entry, read) { $scope.mark = function(entry, read) {
@@ -282,8 +293,8 @@ module.controller('ManageUsersCtrl', function($scope, $state, $location,
}; };
}); });
module.controller('ManageUserCtrl', function($scope, $state, $stateParams, $dialog, module.controller('ManageUserCtrl', function($scope, $state, $stateParams,
AdminUsersService) { $dialog, AdminUsersService) {
$scope.user = $stateParams._id ? AdminUsersService.get({ $scope.user = $stateParams._id ? AdminUsersService.get({
id : $stateParams._id id : $stateParams._id
}) : { }) : {

View File

@@ -173,7 +173,7 @@ module.directive('category', function($compile) {
}; };
}); });
module.directive('toolbar', function($stateParams, $route, $location, module.directive('toolbar', function($state, $stateParams, $route, $location,
SettingsService, EntryService, SubscriptionService, SessionService) { SettingsService, EntryService, SubscriptionService, SessionService) {
return { return {
scope : {}, scope : {},
@@ -208,6 +208,21 @@ module.directive('toolbar', function($stateParams, $route, $location,
}); });
}); });
}; };
$scope.keywords = $stateParams._keywords;
$scope.search = function() {
if ($scope.keywords == $stateParams._keywords) {
$scope.refresh();
} else {
$state.transitionTo('feeds.search', {
_keywords : $scope.keywords
});
}
};
$scope.showButtons = function() {
return !$stateParams._keywords;
}
$scope.toAdmin = function() { $scope.toAdmin = function() {
$location.path('admin'); $location.path('admin');
}; };

View File

@@ -13,6 +13,11 @@ app.config(function($routeProvider, $stateProvider, $urlRouterProvider) {
templateUrl : 'templates/feeds.view.html', templateUrl : 'templates/feeds.view.html',
controller : 'FeedListCtrl' controller : 'FeedListCtrl'
}); });
$stateProvider.state('feeds.search', {
url : '/search/:_keywords',
templateUrl : 'templates/feeds.view.html',
controller : 'FeedListCtrl'
});
$stateProvider.state('admin', { $stateProvider.state('admin', {
abstract : true, abstract : true,

View File

@@ -147,6 +147,12 @@ module.factory('EntryService', function($resource, $http) {
params : { params : {
_method : 'mark' _method : 'mark'
} }
},
search : {
method : 'GET',
params : {
_method : 'search'
}
} }
}; };
var res = $resource('rest/entries/:_method', {}, actions); var res = $resource('rest/entries/:_method', {}, actions);

View File

@@ -8,9 +8,9 @@
<div ng-repeat="entry in entries" class="entry"> <div ng-repeat="entry in entries" class="entry">
<a scroll-to="isOpen && current == entry" scroll-to-offset="-58" href="{{entry.url}}" target="_blank" class="entry-heading" ng-click="entryClicked(entry, $event)" <a scroll-to="isOpen && current == entry" scroll-to-offset="-58" href="{{entry.url}}" target="_blank" class="entry-heading" ng-click="entryClicked(entry, $event)"
ng-class="{open: current == entry, closed: current != entry}"> ng-class="{open: current == entry, closed: current != entry}">
<span ui-if="selectedType == 'category'" class="feed-name">{{entry.feedName}}</span> <span ui-if="keywords || selectedType == 'category'" class="feed-name">{{entry.feedName}}</span>
<span class="entry-date">{{entry.date}}</span> <span class="entry-date">{{entry.date}}</span>
<span class="entry-name" ng-class="{unread: entry.read == false, shrink: selectedType == 'category'}" ng-bind-html-unsafe="entry.title"></span> <span class="entry-name" ng-class="{unread: entry.read == false, shrink: keywords || selectedType == 'category'}" ng-bind-html-unsafe="entry.title"></span>
</a> </a>
<div class="entry-body" ui-if="isOpen && current == entry"> <div class="entry-body" ui-if="isOpen && current == entry">