Hashtags: Refactoring of the REST API
Sending data with a DELETE is not a common standard. Replace the PUT and DELETE endpoints with a POST method which accepts both adds and removals. Instead of sending hashtags as a comma-separated list, send as a set and raise HTTP 400 Bad Request if any hashtag contains a comma. Return results as an ordered list. Having the results in a deterministic order makes it easier to test. Only perform notes db update and indexing if any tags were actually added or removed. Examples: GET /a/changes/1/hashtags HTTP/1.1 Response: HTTP/1.1 200 OK [ "tag1", "tag2" ] POST /a/changes/1/hashtags HTTP/1.1 { "add" : "tag3", "remove" : "tag2" } Response: HTTP/1.1 200 OK [ "tag1", "tag3" ] Change-Id: Idde97697ecc6bc1a51404bef67baf645d8909555
This commit is contained in:
parent
2501e548e2
commit
6b15344e38
gerrit-server/src/main/java/com/google/gerrit/server/change
@ -26,11 +26,12 @@ import com.google.inject.Singleton;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class GetHashtags implements RestReadView<ChangeResource> {
|
public class GetHashtags implements RestReadView<ChangeResource> {
|
||||||
@Override
|
@Override
|
||||||
public Response<Set<String>> apply(ChangeResource req)
|
public Response<? extends Set<String>> apply(ChangeResource req)
|
||||||
throws AuthException, OrmException, IOException, BadRequestException {
|
throws AuthException, OrmException, IOException, BadRequestException {
|
||||||
|
|
||||||
ChangeControl control = req.getControl();
|
ChangeControl control = req.getControl();
|
||||||
@ -39,6 +40,6 @@ public class GetHashtags implements RestReadView<ChangeResource> {
|
|||||||
if (hashtags == null) {
|
if (hashtags == null) {
|
||||||
hashtags = ImmutableSet.of();
|
hashtags = ImmutableSet.of();
|
||||||
}
|
}
|
||||||
return Response.ok(hashtags);
|
return Response.ok(new TreeSet<String>(hashtags));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,11 +53,10 @@ public class Module extends RestApiModule {
|
|||||||
get(CHANGE_KIND, "in").to(IncludedIn.class);
|
get(CHANGE_KIND, "in").to(IncludedIn.class);
|
||||||
get(CHANGE_KIND, "hashtags").to(GetHashtags.class);
|
get(CHANGE_KIND, "hashtags").to(GetHashtags.class);
|
||||||
put(CHANGE_KIND, "topic").to(PutTopic.class);
|
put(CHANGE_KIND, "topic").to(PutTopic.class);
|
||||||
put(CHANGE_KIND, "hashtags").to(PutHashtags.class);
|
|
||||||
delete(CHANGE_KIND, "topic").to(PutTopic.class);
|
delete(CHANGE_KIND, "topic").to(PutTopic.class);
|
||||||
delete(CHANGE_KIND).to(DeleteDraftChange.class);
|
delete(CHANGE_KIND).to(DeleteDraftChange.class);
|
||||||
delete(CHANGE_KIND, "hashtags").to(DeleteHashtags.class);
|
|
||||||
post(CHANGE_KIND, "abandon").to(Abandon.class);
|
post(CHANGE_KIND, "abandon").to(Abandon.class);
|
||||||
|
post(CHANGE_KIND, "hashtags").to(PostHashtags.class);
|
||||||
post(CHANGE_KIND, "publish").to(Publish.CurrentRevision.class);
|
post(CHANGE_KIND, "publish").to(Publish.CurrentRevision.class);
|
||||||
post(CHANGE_KIND, "restore").to(Restore.class);
|
post(CHANGE_KIND, "restore").to(Restore.class);
|
||||||
post(CHANGE_KIND, "revert").to(Revert.class);
|
post(CHANGE_KIND, "revert").to(Revert.class);
|
||||||
|
@ -14,17 +14,14 @@
|
|||||||
|
|
||||||
package com.google.gerrit.server.change;
|
package com.google.gerrit.server.change;
|
||||||
|
|
||||||
import com.google.common.base.CharMatcher;
|
import com.google.common.collect.ImmutableSet;
|
||||||
import com.google.common.base.Splitter;
|
|
||||||
import com.google.common.base.Strings;
|
|
||||||
import com.google.common.collect.Lists;
|
|
||||||
import com.google.gerrit.extensions.restapi.AuthException;
|
import com.google.gerrit.extensions.restapi.AuthException;
|
||||||
import com.google.gerrit.extensions.restapi.BadRequestException;
|
import com.google.gerrit.extensions.restapi.BadRequestException;
|
||||||
import com.google.gerrit.extensions.restapi.DefaultInput;
|
import com.google.gerrit.extensions.restapi.DefaultInput;
|
||||||
import com.google.gerrit.extensions.restapi.Response;
|
import com.google.gerrit.extensions.restapi.Response;
|
||||||
import com.google.gerrit.extensions.restapi.RestModifyView;
|
import com.google.gerrit.extensions.restapi.RestModifyView;
|
||||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||||
import com.google.gerrit.server.change.DeleteHashtags.Input;
|
import com.google.gerrit.server.change.PostHashtags.Input;
|
||||||
import com.google.gerrit.server.index.ChangeIndexer;
|
import com.google.gerrit.server.index.ChangeIndexer;
|
||||||
import com.google.gerrit.server.notedb.ChangeNotes;
|
import com.google.gerrit.server.notedb.ChangeNotes;
|
||||||
import com.google.gerrit.server.notedb.ChangeUpdate;
|
import com.google.gerrit.server.notedb.ChangeUpdate;
|
||||||
@ -37,50 +34,81 @@ import com.google.inject.Singleton;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class DeleteHashtags implements RestModifyView<ChangeResource, Input> {
|
public class PostHashtags implements RestModifyView<ChangeResource, Input> {
|
||||||
private final ChangeUpdate.Factory updateFactory;
|
private final ChangeUpdate.Factory updateFactory;
|
||||||
private final Provider<ReviewDb> dbProvider;
|
private final Provider<ReviewDb> dbProvider;
|
||||||
private final ChangeIndexer indexer;
|
private final ChangeIndexer indexer;
|
||||||
|
|
||||||
public static class Input {
|
public static class Input {
|
||||||
@DefaultInput
|
@DefaultInput
|
||||||
public String hashtags;
|
public Set<String> add;
|
||||||
|
public Set<String> remove;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
DeleteHashtags(ChangeUpdate.Factory updateFactory,
|
PostHashtags(ChangeUpdate.Factory updateFactory,
|
||||||
Provider<ReviewDb> dbProvider, ChangeIndexer indexer) {
|
Provider<ReviewDb> dbProvider, ChangeIndexer indexer) {
|
||||||
this.updateFactory = updateFactory;
|
this.updateFactory = updateFactory;
|
||||||
this.dbProvider = dbProvider;
|
this.dbProvider = dbProvider;
|
||||||
this.indexer = indexer;
|
this.indexer = indexer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Set<String> extractTags(Set<String> input)
|
||||||
|
throws BadRequestException {
|
||||||
|
if (input == null) {
|
||||||
|
return ImmutableSet.of();
|
||||||
|
} else {
|
||||||
|
HashSet<String> result = new HashSet<>();
|
||||||
|
for (String hashtag : input) {
|
||||||
|
if (hashtag.contains(",")) {
|
||||||
|
throw new BadRequestException("Hashtags may not contain commas");
|
||||||
|
}
|
||||||
|
if (!hashtag.trim().isEmpty()) {
|
||||||
|
result.add(hashtag.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Response<Set<String>> apply(ChangeResource req, Input input)
|
public Response<? extends Set<String>> apply(ChangeResource req, Input input)
|
||||||
throws AuthException, OrmException, IOException, BadRequestException {
|
throws AuthException, OrmException, IOException, BadRequestException {
|
||||||
if (input == null || Strings.isNullOrEmpty(input.hashtags)) {
|
if (input == null
|
||||||
|
|| (input.add == null && input.remove == null)) {
|
||||||
throw new BadRequestException("Hashtags are required");
|
throw new BadRequestException("Hashtags are required");
|
||||||
}
|
}
|
||||||
|
|
||||||
ChangeControl control = req.getControl();
|
ChangeControl control = req.getControl();
|
||||||
if(!control.canEditHashtags()){
|
if (!control.canEditHashtags()) {
|
||||||
throw new AuthException("Editing hashtags not permitted");
|
throw new AuthException("Editing hashtags not permitted");
|
||||||
}
|
}
|
||||||
ChangeUpdate update = updateFactory.create(control);
|
ChangeUpdate update = updateFactory.create(control);
|
||||||
ChangeNotes notes = control.getNotes().load();
|
ChangeNotes notes = control.getNotes().load();
|
||||||
Set<String> hashtags = new HashSet<String>();
|
|
||||||
Set<String> oldHashtags = notes.getHashtags();
|
|
||||||
if (oldHashtags != null) {
|
|
||||||
hashtags.addAll(oldHashtags);
|
|
||||||
}
|
|
||||||
hashtags.removeAll(Lists.newArrayList(Splitter.on(CharMatcher.anyOf(",;"))
|
|
||||||
.trimResults().omitEmptyStrings().split(input.hashtags)));
|
|
||||||
update.setHashtags(hashtags);
|
|
||||||
update.commit();
|
|
||||||
indexer.index(dbProvider.get(), update.getChange());
|
|
||||||
|
|
||||||
return Response.ok(hashtags);
|
Set<String> existingHashtags = notes.getHashtags();
|
||||||
|
Set<String> updatedHashtags = new HashSet<>();
|
||||||
|
Set<String> toAdd = new HashSet<>(extractTags(input.add));
|
||||||
|
Set<String> toRemove = new HashSet<>(extractTags(input.remove));
|
||||||
|
|
||||||
|
if (existingHashtags != null && !existingHashtags.isEmpty()) {
|
||||||
|
updatedHashtags.addAll(existingHashtags);
|
||||||
|
toAdd.removeAll(existingHashtags);
|
||||||
|
toRemove.retainAll(existingHashtags);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toAdd.size() > 0 || toRemove.size() > 0) {
|
||||||
|
updatedHashtags.addAll(toAdd);
|
||||||
|
updatedHashtags.removeAll(toRemove);
|
||||||
|
update.setHashtags(updatedHashtags);
|
||||||
|
update.commit();
|
||||||
|
|
||||||
|
indexer.index(dbProvider.get(), update.getChange());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.ok(new TreeSet<String>(updatedHashtags));
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,86 +0,0 @@
|
|||||||
// Copyright (C) 2012 The Android Open Source Project
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package com.google.gerrit.server.change;
|
|
||||||
|
|
||||||
import com.google.common.base.CharMatcher;
|
|
||||||
import com.google.common.base.Splitter;
|
|
||||||
import com.google.common.base.Strings;
|
|
||||||
import com.google.common.collect.Lists;
|
|
||||||
import com.google.gerrit.extensions.restapi.AuthException;
|
|
||||||
import com.google.gerrit.extensions.restapi.BadRequestException;
|
|
||||||
import com.google.gerrit.extensions.restapi.DefaultInput;
|
|
||||||
import com.google.gerrit.extensions.restapi.Response;
|
|
||||||
import com.google.gerrit.extensions.restapi.RestModifyView;
|
|
||||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
|
||||||
import com.google.gerrit.server.change.PutHashtags.Input;
|
|
||||||
import com.google.gerrit.server.index.ChangeIndexer;
|
|
||||||
import com.google.gerrit.server.notedb.ChangeNotes;
|
|
||||||
import com.google.gerrit.server.notedb.ChangeUpdate;
|
|
||||||
import com.google.gerrit.server.project.ChangeControl;
|
|
||||||
import com.google.gwtorm.server.OrmException;
|
|
||||||
import com.google.inject.Inject;
|
|
||||||
import com.google.inject.Provider;
|
|
||||||
import com.google.inject.Singleton;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
public class PutHashtags implements RestModifyView<ChangeResource, Input> {
|
|
||||||
private final ChangeUpdate.Factory updateFactory;
|
|
||||||
private final Provider<ReviewDb> dbProvider;
|
|
||||||
private final ChangeIndexer indexer;
|
|
||||||
|
|
||||||
public static class Input {
|
|
||||||
@DefaultInput
|
|
||||||
public String hashtags;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
PutHashtags(ChangeUpdate.Factory updateFactory,
|
|
||||||
Provider<ReviewDb> dbProvider, ChangeIndexer indexer) {
|
|
||||||
this.updateFactory = updateFactory;
|
|
||||||
this.dbProvider = dbProvider;
|
|
||||||
this.indexer = indexer;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Response<Set<String>> apply(ChangeResource req, Input input)
|
|
||||||
throws AuthException, OrmException, IOException, BadRequestException {
|
|
||||||
if (input == null || Strings.isNullOrEmpty(input.hashtags)) {
|
|
||||||
throw new BadRequestException("Hashtags are required");
|
|
||||||
}
|
|
||||||
|
|
||||||
ChangeControl control = req.getControl();
|
|
||||||
if (!control.canEditHashtags()) {
|
|
||||||
throw new AuthException("Editing hashtags not permitted");
|
|
||||||
}
|
|
||||||
ChangeUpdate update = updateFactory.create(control);
|
|
||||||
ChangeNotes notes = control.getNotes().load();
|
|
||||||
Set<String> oldHashtags = notes.getHashtags();
|
|
||||||
Set<String> hashtags = new HashSet<String>();
|
|
||||||
if (oldHashtags != null) {
|
|
||||||
hashtags.addAll(oldHashtags);
|
|
||||||
};
|
|
||||||
hashtags.addAll(Lists.newArrayList(Splitter.on(CharMatcher.anyOf(",;"))
|
|
||||||
.trimResults().omitEmptyStrings().split(input.hashtags)));
|
|
||||||
update.setHashtags(hashtags);
|
|
||||||
update.commit();
|
|
||||||
indexer.index(dbProvider.get(), update.getChange());
|
|
||||||
|
|
||||||
return Response.ok(hashtags);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user