]> source.dussan.org Git - sonarqube.git/commitdiff
Refactor handling of 'selection mode' for WS actions 345/head
authorJean-Baptiste Lievremont <jean-baptiste.lievremont@sonarsource.com>
Tue, 2 Jun 2015 09:03:34 +0000 (11:03 +0200)
committerJean-Baptiste Lievremont <jean-baptiste.lievremont@sonarsource.com>
Wed, 3 Jun 2015 09:43:26 +0000 (11:43 +0200)
server/sonar-server/src/main/java/org/sonar/server/permission/PermissionQueryParser.java
server/sonar-server/src/main/java/org/sonar/server/qualitygate/ws/QGatesWs.java
server/sonar-server/src/main/java/org/sonar/server/qualitygate/ws/SearchAction.java
server/sonar-server/src/main/java/org/sonar/server/qualityprofile/ws/ProjectsAction.java
server/sonar-server/src/main/java/org/sonar/server/user/ws/GroupsAction.java
server/sonar-server/src/main/java/org/sonar/server/usergroups/ws/UsersAction.java
server/sonar-server/src/test/java/org/sonar/server/user/ws/GroupsActionTest.java
server/sonar-server/src/test/java/org/sonar/server/usergroups/ws/UsersActionTest.java
sonar-plugin-api/src/main/java/org/sonar/api/server/ws/WebService.java

index 0384a735ef6fb0d5cabc1b28084bbc488b58f4de..8d16f383bb6d78336c38401fbdb97e4bffca3794 100644 (file)
@@ -20,6 +20,8 @@
 
 package org.sonar.server.permission;
 
+import org.sonar.api.server.ws.WebService.SelectionMode;
+
 import org.sonar.core.permission.PermissionQuery;
 import org.sonar.core.user.GroupMembershipQuery;
 import org.sonar.server.util.RubyUtils;
@@ -28,9 +30,6 @@ import java.util.Map;
 
 public class PermissionQueryParser {
 
-  private static final String SELECTED_MEMBERSHIP = "selected";
-  private static final String DESELECTED_MEMBERSHIP = "deselected";
-
   private PermissionQueryParser(){
     // Utility class
   }
@@ -48,10 +47,10 @@ public class PermissionQueryParser {
   }
 
   private static String membership(Map<String, Object> params) {
-    String selected = (String) params.get("selected");
-    if (SELECTED_MEMBERSHIP.equals(selected)) {
+    SelectionMode selectionMode = SelectionMode.fromParam((String) params.get("selected"));
+    if (SelectionMode.SELECTED == selectionMode) {
       return GroupMembershipQuery.IN;
-    } else if (DESELECTED_MEMBERSHIP.equals(selected)) {
+    } else if (SelectionMode.DESELECTED == selectionMode) {
       return GroupMembershipQuery.OUT;
     } else {
       return GroupMembershipQuery.ANY;
index bb8bff8abba59f0d28119bdbcb452f215c9476fb..514e8d4a6556f93fda0323991ae5e4ba41e9adbb 100644 (file)
@@ -31,7 +31,6 @@ public class QGatesWs implements WebService {
   static final String PARAM_PAGE_SIZE = "pageSize";
   static final String PARAM_PAGE = "page";
   static final String PARAM_QUERY = "query";
-  static final String PARAM_SELECTED = "selected";
   static final String PARAM_NAME = "name";
   static final String PARAM_ERROR = "error";
   static final String PARAM_WARNING = "warning";
index 24ccf061c5302379d3e2d4c169b2ce00ef1692df..69268b929ec559231c28daed186d14aeadbc1899 100644 (file)
@@ -20,6 +20,8 @@
 
 package org.sonar.server.qualitygate.ws;
 
+import org.sonar.api.server.ws.WebService.Param;
+
 import com.google.common.io.Resources;
 import org.sonar.api.server.ws.Request;
 import org.sonar.api.server.ws.Response;
@@ -54,11 +56,7 @@ public class SearchAction implements QGateWsAction {
       .setDescription("To search for projects containing this string. If this parameter is set, \"selected\" is set to \"all\".")
       .setExampleValue("abc");
 
-    action.createParam(QGatesWs.PARAM_SELECTED)
-      .setDescription("If \"selected\", search for projects associated to the quality gate")
-      .setDefaultValue(ProjectQgateAssociationQuery.IN)
-      .setPossibleValues(ProjectQgateAssociationQuery.AVAILABLE_MEMBERSHIP)
-      .setExampleValue(ProjectQgateAssociationQuery.OUT);
+    action.addSelectionModeParam();
 
     action.createParam(QGatesWs.PARAM_PAGE)
       .setDescription("Page number")
@@ -74,7 +72,7 @@ public class SearchAction implements QGateWsAction {
   public void handle(Request request, Response response) {
     QgateProjectFinder.Association associations = projectFinder.find(ProjectQgateAssociationQuery.builder()
       .gateId(request.mandatoryParam(QGatesWs.PARAM_GATE_ID))
-      .membership(request.param(QGatesWs.PARAM_QUERY) == null ? request.param(QGatesWs.PARAM_SELECTED) : ProjectQgateAssociationQuery.ANY)
+      .membership(request.param(QGatesWs.PARAM_QUERY) == null ? request.param(Param.SELECTED) : ProjectQgateAssociationQuery.ANY)
       .projectSearch(request.param(QGatesWs.PARAM_QUERY))
       .pageIndex(request.paramAsInt(QGatesWs.PARAM_PAGE))
       .pageSize(request.paramAsInt(QGatesWs.PARAM_PAGE_SIZE))
@@ -83,7 +81,7 @@ public class SearchAction implements QGateWsAction {
     writer.beginObject().prop("more", associations.hasMoreResults());
     writer.name("results").beginArray();
     for (ProjectQgateAssociation project : associations.projects()) {
-      writer.beginObject().prop("id", project.id()).prop("name", project.name()).prop(QGatesWs.PARAM_SELECTED, project.isMember()).endObject();
+      writer.beginObject().prop("id", project.id()).prop("name", project.name()).prop(Param.SELECTED, project.isMember()).endObject();
     }
     writer.endArray().endObject().close();
   }
index f4b3bc9658f35cde0e0450f6e1664c318fdbd85a..fb6349e77e4421d7147315f6f6938903d0e746e2 100644 (file)
@@ -19,6 +19,9 @@
  */
 package org.sonar.server.qualityprofile.ws;
 
+import org.sonar.api.server.ws.WebService.Param;
+
+import org.sonar.api.server.ws.WebService.SelectionMode;
 import com.google.common.base.Predicate;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.Iterables;
@@ -46,15 +49,10 @@ import java.util.List;
 public class ProjectsAction implements QProfileWsAction {
 
   private static final String PARAM_KEY = "key";
-  private static final String PARAM_SELECTED = "selected";
   private static final String PARAM_QUERY = "query";
   private static final String PARAM_PAGE_SIZE = "pageSize";
   private static final String PARAM_PAGE = "page";
 
-  private static final String SELECTION_ALL = "all";
-  private static final String SELECTION_SELECTED = "selected";
-  private static final String SELECTION_DESELECTED = "deselected";
-
   private final DbClient dbClient;
   private final UserSession userSession;
 
@@ -74,10 +72,7 @@ public class ProjectsAction implements QProfileWsAction {
       .setDescription("A quality profile key.")
       .setRequired(true)
       .setExampleValue("sonar-way-java-12345");
-    projects.createParam(PARAM_SELECTED)
-      .setDescription("If specified, return only selected or deselected projects.")
-      .setPossibleValues(SELECTION_SELECTED, SELECTION_DESELECTED, SELECTION_ALL)
-      .setDefaultValue(SELECTION_ALL);
+    projects.addSelectionModeParam();
     projects.createParam(PARAM_QUERY)
       .setDescription("If specified, return only projects whose name match the query.");
     projects.createParam(PARAM_PAGE_SIZE)
@@ -94,7 +89,7 @@ public class ProjectsAction implements QProfileWsAction {
 
     try {
       checkProfileExists(profileKey, session);
-      String selected = request.param(PARAM_SELECTED);
+      String selected = request.param(Param.SELECTED);
       String query = request.param(PARAM_QUERY);
       int pageSize = request.mandatoryParamAsInt(PARAM_PAGE_SIZE);
       int page = request.mandatoryParamAsInt(PARAM_PAGE);
@@ -151,9 +146,10 @@ public class ProjectsAction implements QProfileWsAction {
 
   private List<ProjectQprofileAssociationDto> loadProjects(String profileKey, DbSession session, String selected, String query) {
     List<ProjectQprofileAssociationDto> projects = Lists.newArrayList();
-    if (SELECTION_SELECTED.equals(selected)) {
+    SelectionMode selectionMode = SelectionMode.fromParam(selected);
+    if (SelectionMode.SELECTED == selectionMode) {
       projects.addAll(dbClient.qualityProfileDao().selectSelectedProjects(profileKey, query, session));
-    } else if (SELECTION_DESELECTED.equals(selected)) {
+    } else if (SelectionMode.DESELECTED == selectionMode) {
       projects.addAll(dbClient.qualityProfileDao().selectDeselectedProjects(profileKey, query, session));
     } else {
       projects.addAll(dbClient.qualityProfileDao().selectProjectAssociations(profileKey, query, session));
index 9538cd931045dac81fa7a157ead36ea39183277d..b713e9a6ed2ad44be9acf159997c3702e5d9ece6 100644 (file)
 package org.sonar.server.user.ws;
 
 import java.util.List;
-import javax.annotation.Nullable;
 import org.sonar.api.server.ws.Request;
 import org.sonar.api.server.ws.Response;
 import org.sonar.api.server.ws.WebService.NewAction;
 import org.sonar.api.server.ws.WebService.NewController;
 import org.sonar.api.server.ws.WebService.Param;
+import org.sonar.api.server.ws.WebService.SelectionMode;
 import org.sonar.api.utils.Paging;
 import org.sonar.api.utils.text.JsonWriter;
 import org.sonar.core.permission.GlobalPermissions;
@@ -39,15 +39,10 @@ import org.sonar.server.user.UserSession;
 public class GroupsAction implements UsersWsAction {
 
   private static final String PARAM_LOGIN = "login";
-  private static final String PARAM_SELECTED = "selected";
 
-  private static final String SELECTION_ALL = "all";
-  private static final String SELECTION_SELECTED = PARAM_SELECTED;
-  private static final String SELECTION_DESELECTED = "deselected";
-
-  private static final String FIELD_SELECTED = PARAM_SELECTED;
-  private static final String FIELD_DESCRIPTION = "description";
   private static final String FIELD_NAME = "name";
+  private static final String FIELD_DESCRIPTION = "description";
+  private static final String FIELD_SELECTED = "selected";
 
   private final DbClient dbClient;
   private final UserSession userSession;
@@ -70,14 +65,9 @@ public class GroupsAction implements UsersWsAction {
       .setExampleValue("admin")
       .setRequired(true);
 
-    action.createParam(PARAM_SELECTED)
-      .setDescription("If specified, only show groups the user is member of (selected) or not (deselected).")
-      .setPossibleValues(SELECTION_SELECTED, SELECTION_DESELECTED, SELECTION_ALL)
-      .setDefaultValue(SELECTION_ALL);
+    action.addSelectionModeParam();
 
-    action.createParam(Param.TEXT_QUERY)
-      .setDescription("If specified, only show groups whose name contains the query.")
-      .setExampleValue("user");
+    action.addSearchQuery("users", "group names");
 
     action.addPagingParams(25);
   }
@@ -90,7 +80,7 @@ public class GroupsAction implements UsersWsAction {
     int pageSize = request.mandatoryParamAsInt(Param.PAGE_SIZE);
     int page = request.mandatoryParamAsInt(Param.PAGE);
     String queryString = request.param(Param.TEXT_QUERY);
-    String selected = request.param(PARAM_SELECTED);
+    String selected = request.mandatoryParam(Param.SELECTED);
 
     GroupMembershipQuery query = GroupMembershipQuery.builder()
       .login(login)
@@ -116,7 +106,7 @@ public class GroupsAction implements UsersWsAction {
     }
   }
 
-  private void writeGroups(JsonWriter json, List<GroupMembershipDto> groups) {
+  private static void writeGroups(JsonWriter json, List<GroupMembershipDto> groups) {
     json.name("groups").beginArray();
     for (GroupMembershipDto group : groups) {
       json.beginObject()
@@ -128,17 +118,18 @@ public class GroupsAction implements UsersWsAction {
     json.endArray();
   }
 
-  private void writePaging(JsonWriter json, Paging paging) {
+  private static void writePaging(JsonWriter json, Paging paging) {
     json.prop("p", paging.pageIndex())
       .prop("ps", paging.pageSize())
       .prop("total", paging.total());
   }
 
-  private String getMembership(@Nullable String selected) {
+  private String getMembership(String selected) {
+    SelectionMode selectionMode = SelectionMode.fromParam(selected);
     String membership = GroupMembershipQuery.ANY;
-    if (SELECTION_SELECTED.equals(selected)) {
+    if (SelectionMode.SELECTED == selectionMode) {
       membership = GroupMembershipQuery.IN;
-    } else if (SELECTION_DESELECTED.equals(selected)) {
+    } else if (SelectionMode.DESELECTED == selectionMode) {
       membership = GroupMembershipQuery.OUT;
     }
     return membership;
index 526ddd114b980a6a0e9326c1c38ceefa895aff0c..6cb3a52d35f24eb3d856b93531b64086c05c1d37 100644 (file)
 package org.sonar.server.usergroups.ws;
 
 import java.util.List;
-import javax.annotation.Nullable;
 import org.sonar.api.server.ws.Request;
 import org.sonar.api.server.ws.Response;
 import org.sonar.api.server.ws.WebService.NewAction;
 import org.sonar.api.server.ws.WebService.NewController;
 import org.sonar.api.server.ws.WebService.Param;
+import org.sonar.api.server.ws.WebService.SelectionMode;
 import org.sonar.api.utils.Paging;
 import org.sonar.api.utils.text.JsonWriter;
 import org.sonar.core.permission.GlobalPermissions;
@@ -40,13 +40,8 @@ import org.sonar.server.user.UserSession;
 public class UsersAction implements UserGroupsWsAction {
 
   private static final String PARAM_ID = "id";
-  private static final String PARAM_SELECTED = "selected";
 
-  private static final String SELECTION_ALL = "all";
-  private static final String SELECTION_SELECTED = PARAM_SELECTED;
-  private static final String SELECTION_DESELECTED = "deselected";
-
-  private static final String FIELD_SELECTED = PARAM_SELECTED;
+  private static final String FIELD_SELECTED = "selected";
   private static final String FIELD_NAME = "name";
   private static final String FIELD_LOGIN = "login";
 
@@ -71,10 +66,7 @@ public class UsersAction implements UserGroupsWsAction {
       .setExampleValue("42")
       .setRequired(true);
 
-    action.createParam(PARAM_SELECTED)
-      .setDescription("If specified, only show users who belong to a group (selected=selected) or only those who do not (selected=deselected).")
-      .setPossibleValues(SELECTION_SELECTED, SELECTION_DESELECTED, SELECTION_ALL)
-      .setDefaultValue(SELECTION_ALL);
+    action.addSelectionModeParam();
 
     action.addSearchQuery("freddy", "names", "logins");
 
@@ -89,7 +81,7 @@ public class UsersAction implements UserGroupsWsAction {
     int pageSize = request.mandatoryParamAsInt(Param.PAGE_SIZE);
     int page = request.mandatoryParamAsInt(Param.PAGE);
     String queryString = request.param(Param.TEXT_QUERY);
-    String selected = request.param(PARAM_SELECTED);
+    String selected = request.mandatoryParam(Param.SELECTED);
 
     UserMembershipQuery query = UserMembershipQuery.builder()
       .groupId(groupId)
@@ -115,7 +107,7 @@ public class UsersAction implements UserGroupsWsAction {
     }
   }
 
-  private void writeMembers(JsonWriter json, List<UserMembershipDto> users) {
+  private static void writeMembers(JsonWriter json, List<UserMembershipDto> users) {
     json.name("users").beginArray();
     for (UserMembershipDto user : users) {
       json.beginObject()
@@ -127,17 +119,18 @@ public class UsersAction implements UserGroupsWsAction {
     json.endArray();
   }
 
-  private void writePaging(JsonWriter json, Paging paging) {
+  private static void writePaging(JsonWriter json, Paging paging) {
     json.prop(Param.PAGE, paging.pageIndex())
       .prop(Param.PAGE_SIZE, paging.pageSize())
       .prop("total", paging.total());
   }
 
-  private String getMembership(@Nullable String selected) {
+  private String getMembership(String selected) {
+    SelectionMode selectionMode = SelectionMode.fromParam(selected);
     String membership = GroupMembershipQuery.ANY;
-    if (SELECTION_SELECTED.equals(selected)) {
+    if (SelectionMode.SELECTED == selectionMode) {
       membership = GroupMembershipQuery.IN;
-    } else if (SELECTION_DESELECTED.equals(selected)) {
+    } else if (SelectionMode.DESELECTED == selectionMode) {
       membership = GroupMembershipQuery.OUT;
     }
     return membership;
index c35d5cb0a06b98c586be5602be3fb95fda8d6751..d57b5baa0822be5943153453e57b009e74dfc044 100644 (file)
@@ -26,6 +26,7 @@ import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
 import org.sonar.api.server.ws.WebService.Param;
+import org.sonar.api.server.ws.WebService.SelectionMode;
 import org.sonar.api.utils.System2;
 import org.sonar.core.permission.GlobalPermissions;
 import org.sonar.core.persistence.DbSession;
@@ -111,6 +112,7 @@ public class GroupsActionTest {
 
     tester.newGetRequest("api/users", "groups")
       .setParam("login", "john")
+      .setParam(Param.SELECTED, SelectionMode.ALL.value())
       .execute()
       .assertJson(getClass(), "all.json");
   }
@@ -125,7 +127,12 @@ public class GroupsActionTest {
 
     tester.newGetRequest("api/users", "groups")
       .setParam("login", "john")
-      .setParam("selected", "selected")
+      .execute()
+      .assertJson(getClass(), "selected.json");
+
+    tester.newGetRequest("api/users", "groups")
+      .setParam("login", "john")
+      .setParam(Param.SELECTED, SelectionMode.SELECTED.value())
       .execute()
       .assertJson(getClass(), "selected.json");
   }
@@ -140,7 +147,7 @@ public class GroupsActionTest {
 
     tester.newGetRequest("api/users", "groups")
       .setParam("login", "john")
-      .setParam("selected", "deselected")
+      .setParam(Param.SELECTED, SelectionMode.DESELECTED.value())
       .execute()
       .assertJson(getClass(), "deselected.json");
   }
@@ -165,6 +172,7 @@ public class GroupsActionTest {
     tester.newGetRequest("api/users", "groups")
       .setParam("login", "john")
       .setParam(Param.PAGE_SIZE, "1")
+      .setParam(Param.SELECTED, SelectionMode.ALL.value())
       .execute()
       .assertJson(getClass(), "all_page1.json");
 
@@ -172,6 +180,7 @@ public class GroupsActionTest {
       .setParam("login", "john")
       .setParam(Param.PAGE_SIZE, "1")
       .setParam(Param.PAGE, "2")
+      .setParam(Param.SELECTED, SelectionMode.ALL.value())
       .execute()
       .assertJson(getClass(), "all_page2.json");
   }
@@ -187,12 +196,14 @@ public class GroupsActionTest {
     tester.newGetRequest("api/users", "groups")
       .setParam("login", "john")
       .setParam("q", "users")
+      .setParam(Param.SELECTED, SelectionMode.ALL.value())
       .execute()
       .assertJson(getClass(), "all_users.json");
 
     tester.newGetRequest("api/users", "groups")
       .setParam("login", "john")
       .setParam("q", "admin")
+      .setParam(Param.SELECTED, SelectionMode.ALL.value())
       .execute()
       .assertJson(getClass(), "all_admin.json");
   }
index 30c31d346cc84977b5d898d0b31e8a7c5dbfff99..c7288d6d8189e1bd60560e99f0f07193bb8cf32f 100644 (file)
@@ -26,6 +26,8 @@ import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.experimental.categories.Category;
+import org.sonar.api.server.ws.WebService.Param;
+import org.sonar.api.server.ws.WebService.SelectionMode;
 import org.sonar.api.utils.System2;
 import org.sonar.core.permission.GlobalPermissions;
 import org.sonar.core.persistence.DbSession;
@@ -122,6 +124,7 @@ public class UsersActionTest {
 
     newUsersRequest()
       .setParam("id", group.getId().toString())
+      .setParam(Param.SELECTED, SelectionMode.ALL.value())
       .execute()
       .assertJson(getClass(), "all.json");
   }
@@ -136,7 +139,12 @@ public class UsersActionTest {
 
     newUsersRequest()
       .setParam("id", group.getId().toString())
-      .setParam("selected", "selected")
+      .execute()
+      .assertJson(getClass(), "selected.json");
+
+    newUsersRequest()
+      .setParam("id", group.getId().toString())
+      .setParam(Param.SELECTED, SelectionMode.SELECTED.value())
       .execute()
       .assertJson(getClass(), "selected.json");
   }
@@ -151,7 +159,7 @@ public class UsersActionTest {
 
     newUsersRequest()
       .setParam("id", group.getId().toString())
-      .setParam("selected", "deselected")
+      .setParam(Param.SELECTED, SelectionMode.DESELECTED.value())
       .execute()
       .assertJson(getClass(), "deselected.json");
   }
@@ -167,6 +175,7 @@ public class UsersActionTest {
     newUsersRequest()
       .setParam("id", group.getId().toString())
       .setParam("ps", "1")
+      .setParam(Param.SELECTED, SelectionMode.ALL.value())
       .execute()
       .assertJson(getClass(), "all_page1.json");
 
@@ -174,6 +183,7 @@ public class UsersActionTest {
       .setParam("id", group.getId().toString())
       .setParam("ps", "1")
       .setParam("p", "2")
+      .setParam(Param.SELECTED, SelectionMode.ALL.value())
       .execute()
       .assertJson(getClass(), "all_page2.json");
   }
@@ -189,6 +199,7 @@ public class UsersActionTest {
     newUsersRequest()
       .setParam("id", group.getId().toString())
       .setParam("q", "ace")
+      .setParam(Param.SELECTED, SelectionMode.ALL.value())
       .execute()
       .assertJson(getClass(), "all.json");
 
index 49c9c0adf2cf2dcdbb49bf2a05174e002c69b1a5..418caff6e84d7396c4d2dab8bb7d18019106351f 100644 (file)
@@ -19,7 +19,9 @@
  */
 package org.sonar.api.server.ws;
 
+import com.google.common.base.Function;
 import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Maps;
@@ -33,6 +35,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 import org.apache.commons.io.FilenameUtils;
@@ -385,6 +388,18 @@ public interface WebService extends Definable<WebService.Context> {
         .setDefaultValue(defaultAscending);
       return this;
     }
+
+    /**
+     * Add 'selected=(selected|deselected|all)' for select-list oriented WS.
+     */
+    public NewAction addSelectionModeParam() {
+      createParam(Param.SELECTED)
+        .setDescription("Depending on the value, show only selected items (selected=selected), deselected items (selected=deselected), " +
+          "or all items with their selection status (selected=all).")
+        .setDefaultValue(SelectionMode.SELECTED.value())
+        .setPossibleValues(SelectionMode.possibleValues());
+      return this;
+    }
   }
 
   @Immutable
@@ -592,6 +607,36 @@ public interface WebService extends Definable<WebService.Context> {
     }
   }
 
+  enum SelectionMode {
+    SELECTED("selected"), DESELECTED("deselected"), ALL("all");
+
+    private final String paramValue;
+
+    private static final Map<String, SelectionMode> BY_VALUE = Maps.uniqueIndex(Arrays.asList(values()), new Function<SelectionMode, String>() {
+      @Override
+      public String apply(@Nonnull SelectionMode input) {
+        return input.paramValue;
+      }
+    });
+
+    private SelectionMode(String paramValue) {
+      this.paramValue = paramValue;
+    }
+
+    public String value() {
+      return paramValue;
+    }
+
+    public static SelectionMode fromParam(String paramValue) {
+      Preconditions.checkArgument(BY_VALUE.containsKey(paramValue));
+      return BY_VALUE.get(paramValue);
+    }
+
+    public static Collection<String> possibleValues() {
+      return BY_VALUE.keySet();
+    }
+  }
+
   @Immutable
   class Param {
     public static final String TEXT_QUERY = "q";
@@ -601,6 +646,7 @@ public interface WebService extends Definable<WebService.Context> {
     public static final String SORT = "s";
     public static final String ASCENDING = "asc";
     public static final String FACETS = "facets";
+    public static final String SELECTED = "selected";
 
     private final String key, deprecatedKey, description, exampleValue, defaultValue;
     private final boolean required;