forked from Archives/Athou_commafeed
interface for deleting duplicates
This commit is contained in:
@@ -7,10 +7,15 @@ import javax.ejb.Stateless;
|
|||||||
import javax.persistence.Query;
|
import javax.persistence.Query;
|
||||||
import javax.persistence.TypedQuery;
|
import javax.persistence.TypedQuery;
|
||||||
import javax.persistence.criteria.CriteriaQuery;
|
import javax.persistence.criteria.CriteriaQuery;
|
||||||
|
import javax.persistence.criteria.Expression;
|
||||||
import javax.persistence.criteria.JoinType;
|
import javax.persistence.criteria.JoinType;
|
||||||
|
import javax.persistence.criteria.Path;
|
||||||
import javax.persistence.criteria.Predicate;
|
import javax.persistence.criteria.Predicate;
|
||||||
import javax.persistence.criteria.Root;
|
import javax.persistence.criteria.Root;
|
||||||
import javax.persistence.criteria.SetJoin;
|
import javax.persistence.criteria.SetJoin;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
import javax.xml.bind.annotation.XmlRootElement;
|
||||||
|
|
||||||
import org.apache.commons.codec.digest.DigestUtils;
|
import org.apache.commons.codec.digest.DigestUtils;
|
||||||
import org.apache.commons.lang.StringUtils;
|
import org.apache.commons.lang.StringUtils;
|
||||||
@@ -26,6 +31,13 @@ import com.google.common.collect.Lists;
|
|||||||
@Stateless
|
@Stateless
|
||||||
public class FeedDAO extends GenericDAO<Feed> {
|
public class FeedDAO extends GenericDAO<Feed> {
|
||||||
|
|
||||||
|
@XmlRootElement
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
public static class FeedCount {
|
||||||
|
public String normalizedUrlHash;
|
||||||
|
public List<Feed> feeds;
|
||||||
|
}
|
||||||
|
|
||||||
private List<Predicate> getUpdatablePredicates(Root<Feed> root,
|
private List<Predicate> getUpdatablePredicates(Root<Feed> root,
|
||||||
Date threshold) {
|
Date threshold) {
|
||||||
|
|
||||||
@@ -123,4 +135,29 @@ public class FeedDAO extends GenericDAO<Feed> {
|
|||||||
return deleted;
|
return deleted;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<FeedCount> findDuplicates(int offset, int limit) {
|
||||||
|
CriteriaQuery<String> query = builder.createQuery(String.class);
|
||||||
|
Root<Feed> root = query.from(getType());
|
||||||
|
|
||||||
|
Path<String> hashPath = root.get(Feed_.normalizedUrlHash);
|
||||||
|
Expression<Long> count = builder.count(hashPath);
|
||||||
|
|
||||||
|
query.select(hashPath);
|
||||||
|
|
||||||
|
query.groupBy(hashPath);
|
||||||
|
query.having(builder.greaterThan(count, 1l));
|
||||||
|
|
||||||
|
TypedQuery<String> q = em.createQuery(query);
|
||||||
|
limit(q, offset, limit);
|
||||||
|
List<String> normalizedUrlHashes = q.getResultList();
|
||||||
|
|
||||||
|
List<FeedCount> result = Lists.newArrayList();
|
||||||
|
for (String hash : normalizedUrlHashes) {
|
||||||
|
FeedCount fc = new FeedCount();
|
||||||
|
fc.normalizedUrlHash = hash;
|
||||||
|
fc.feeds = findByField(Feed_.normalizedUrlHash, hash);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import com.commafeed.backend.DatabaseCleaner;
|
|||||||
import com.commafeed.backend.MetricsBean;
|
import com.commafeed.backend.MetricsBean;
|
||||||
import com.commafeed.backend.StartupBean;
|
import com.commafeed.backend.StartupBean;
|
||||||
import com.commafeed.backend.dao.FeedDAO;
|
import com.commafeed.backend.dao.FeedDAO;
|
||||||
|
import com.commafeed.backend.dao.FeedDAO.FeedCount;
|
||||||
import com.commafeed.backend.dao.UserDAO;
|
import com.commafeed.backend.dao.UserDAO;
|
||||||
import com.commafeed.backend.dao.UserRoleDAO;
|
import com.commafeed.backend.dao.UserRoleDAO;
|
||||||
import com.commafeed.backend.feeds.FeedRefreshTaskGiver;
|
import com.commafeed.backend.feeds.FeedRefreshTaskGiver;
|
||||||
@@ -38,8 +39,8 @@ import com.commafeed.frontend.SecurityCheck;
|
|||||||
import com.commafeed.frontend.model.UserModel;
|
import com.commafeed.frontend.model.UserModel;
|
||||||
import com.commafeed.frontend.model.request.FeedMergeRequest;
|
import com.commafeed.frontend.model.request.FeedMergeRequest;
|
||||||
import com.commafeed.frontend.model.request.IDRequest;
|
import com.commafeed.frontend.model.request.IDRequest;
|
||||||
import com.google.api.client.util.Lists;
|
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
import com.google.common.collect.Sets;
|
import com.google.common.collect.Sets;
|
||||||
import com.wordnik.swagger.annotations.Api;
|
import com.wordnik.swagger.annotations.Api;
|
||||||
@@ -268,17 +269,36 @@ public class AdminREST extends AbstractResourceREST {
|
|||||||
return Response.ok(map).build();
|
return Response.ok(map).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Path("/cleanup/findDuplicateFeeds")
|
||||||
|
@GET
|
||||||
|
@ApiOperation(value = "Find duplicate feeds")
|
||||||
|
public Response findDuplicateFeeds(@QueryParam("page") int page,
|
||||||
|
@QueryParam("limit") int limit) {
|
||||||
|
List<FeedCount> list = feedDAO.findDuplicates(limit * page, limit);
|
||||||
|
return Response.ok(list).build();
|
||||||
|
}
|
||||||
|
|
||||||
@Path("/cleanup/merge")
|
@Path("/cleanup/merge")
|
||||||
@POST
|
@POST
|
||||||
@ApiOperation(value = "Merge feeds", notes = "Merge feeds together")
|
@ApiOperation(value = "Merge feeds", notes = "Merge feeds together")
|
||||||
public Response mergeFeeds(@ApiParam(required = true) FeedMergeRequest request) {
|
public Response mergeFeeds(
|
||||||
|
@ApiParam(required = true) FeedMergeRequest request) {
|
||||||
Feed into = feedDAO.findById(request.getIntoFeedId());
|
Feed into = feedDAO.findById(request.getIntoFeedId());
|
||||||
|
if (into == null) {
|
||||||
|
return Response.status(Status.BAD_REQUEST)
|
||||||
|
.entity("'into feed' not found").build();
|
||||||
|
}
|
||||||
|
|
||||||
List<Feed> feeds = Lists.newArrayList();
|
List<Feed> feeds = Lists.newArrayList();
|
||||||
for (Long feedId : request.getFeedIds()) {
|
for (Long feedId : request.getFeedIds()) {
|
||||||
Feed feed = feedDAO.findById(feedId);
|
Feed feed = feedDAO.findById(feedId);
|
||||||
feeds.add(feed);
|
feeds.add(feed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (feeds.isEmpty()) {
|
||||||
|
return Response.status(Status.BAD_REQUEST)
|
||||||
|
.entity("'from feeds' empty").build();
|
||||||
|
}
|
||||||
|
|
||||||
cleaner.mergeFeeds(into, feeds);
|
cleaner.mergeFeeds(into, feeds);
|
||||||
return Response.ok().build();
|
return Response.ok().build();
|
||||||
|
|||||||
@@ -1204,6 +1204,34 @@ function($scope, $state, $stateParams, $dialog, AdminUsersService) {
|
|||||||
};
|
};
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
|
module.controller('ManageDuplicateFeedsCtrl', [
|
||||||
|
'$scope', 'AdminCleanupService',
|
||||||
|
function($scope, AdminCleanupService) {
|
||||||
|
|
||||||
|
$scope.limit = 10;
|
||||||
|
$scope.page = 0;
|
||||||
|
$scope.mergeData = {};
|
||||||
|
$scope.refreshData = function() {
|
||||||
|
AdminCleanupService.findDuplicateFeeds({
|
||||||
|
limit : $scope.limit,
|
||||||
|
page : $scope.page
|
||||||
|
}, function(data) {
|
||||||
|
$scope.counts = data;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.focus = function(count) {
|
||||||
|
$scope.current = count;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.merge = function() {
|
||||||
|
AdminCleanupService.mergeFeeds($scope.mergeData, function() {
|
||||||
|
alert('done!');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
}]);
|
||||||
|
|
||||||
module.controller('SettingsCtrl', ['$scope', '$location', 'SettingsService', 'AnalyticsService', 'ServerService',
|
module.controller('SettingsCtrl', ['$scope', '$location', 'SettingsService', 'AnalyticsService', 'ServerService',
|
||||||
function($scope, $location, SettingsService, AnalyticsService, ServerService) {
|
function($scope, $location, SettingsService, AnalyticsService, ServerService) {
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,11 @@ app.config([ '$routeProvider', '$stateProvider', '$urlRouterProvider', '$httpPro
|
|||||||
templateUrl : 'templates/admin.useredit.html',
|
templateUrl : 'templates/admin.useredit.html',
|
||||||
controller : 'ManageUserCtrl'
|
controller : 'ManageUserCtrl'
|
||||||
});
|
});
|
||||||
|
$stateProvider.state('admin.duplicate_feeds', {
|
||||||
|
url : '/feeds/duplicates/',
|
||||||
|
templateUrl : 'templates/admin.duplicate_feeds.html',
|
||||||
|
controller : 'ManageDuplicateFeedsCtrl'
|
||||||
|
});
|
||||||
$stateProvider.state('admin.settings', {
|
$stateProvider.state('admin.settings', {
|
||||||
url : '/settings',
|
url : '/settings',
|
||||||
templateUrl : 'templates/admin.settings.html',
|
templateUrl : 'templates/admin.settings.html',
|
||||||
|
|||||||
@@ -272,6 +272,26 @@ module.factory('AdminSettingsService', ['$resource', function($resource) {
|
|||||||
return res;
|
return res;
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
|
module.factory('AdminCleanupService', ['$resource', function($resource) {
|
||||||
|
var actions = {
|
||||||
|
findDuplicateFeeds : {
|
||||||
|
method : 'GET',
|
||||||
|
isArray: true,
|
||||||
|
params : {
|
||||||
|
_method : 'findDuplicateFeeds'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mergeFeeds : {
|
||||||
|
method : 'POST',
|
||||||
|
params : {
|
||||||
|
_method : 'merge'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
var res = $resource('rest/admin/cleanup/:_method', {}, actions);
|
||||||
|
return res;
|
||||||
|
}]);
|
||||||
|
|
||||||
module.factory('ServerService', ['$resource', function($resource) {
|
module.factory('ServerService', ['$resource', function($resource) {
|
||||||
var res = $resource('rest/server/get');
|
var res = $resource('rest/server/get');
|
||||||
return res;
|
return res;
|
||||||
|
|||||||
34
src/main/webapp/templates/admin.duplicate_feeds.html
Normal file
34
src/main/webapp/templates/admin.duplicate_feeds.html
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<div class="row">
|
||||||
|
|
||||||
|
Limit <input type="number" ng-model="limit" />
|
||||||
|
Page <input type="number" ng-model="page" />
|
||||||
|
<input type="button" class="btn" ng-click="refreshData()" value="Refresh" />
|
||||||
|
<table class="table table-condensed table-hover" ui-if="counts">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>url</th>
|
||||||
|
<th>count</h>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr ng-repeat="count in counts" ng-click="focus(count)" class="pointer">
|
||||||
|
<td>{{count.normalizedUrlHash}}</td>
|
||||||
|
<td>{{count.feeds.length}}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div ui-if="current">
|
||||||
|
<div>
|
||||||
|
Merge
|
||||||
|
<select ng-model="mergeData.feedIds" size="15" multiple="multiple"
|
||||||
|
ng-options="feed.id as feed.url for feed in current.feeds">
|
||||||
|
</select>
|
||||||
|
|
||||||
|
Into
|
||||||
|
<select ng-model="mergeData.intoFeedId" ng-options="feed.id as feed.url for feed in current.feeds">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<input type="button" class="btn" ng-click="merge()" value="Merge" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user