001/* 002 * SonarQube 003 * Copyright (C) 2009-2018 SonarSource SA 004 * mailto:info AT sonarsource DOT com 005 * 006 * This program is free software; you can redistribute it and/or 007 * modify it under the terms of the GNU Lesser General Public 008 * License as published by the Free Software Foundation; either 009 * version 3 of the License, or (at your option) any later version. 010 * 011 * This program is distributed in the hope that it will be useful, 012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 014 * Lesser General Public License for more details. 015 * 016 * You should have received a copy of the GNU Lesser General Public License 017 * along with this program; if not, write to the Free Software Foundation, 018 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 019 */ 020package org.sonar.api.server.ws; 021 022import java.io.IOException; 023import java.net.URL; 024import java.nio.charset.StandardCharsets; 025import java.util.ArrayList; 026import java.util.Collection; 027import java.util.Collections; 028import java.util.HashMap; 029import java.util.LinkedHashSet; 030import java.util.List; 031import java.util.Map; 032import java.util.Objects; 033import java.util.Set; 034import java.util.stream.Collectors; 035import javax.annotation.CheckForNull; 036import javax.annotation.Nullable; 037import javax.annotation.concurrent.Immutable; 038import org.apache.commons.io.FilenameUtils; 039import org.apache.commons.io.IOUtils; 040import org.apache.commons.lang.StringUtils; 041import org.sonar.api.ExtensionPoint; 042import org.sonar.api.server.ServerSide; 043import org.sonar.api.utils.log.Logger; 044import org.sonar.api.utils.log.Loggers; 045 046import static com.google.common.base.Preconditions.checkArgument; 047import static com.google.common.base.Preconditions.checkState; 048import static com.google.common.base.Strings.isNullOrEmpty; 049import static java.lang.String.format; 050import static java.util.Arrays.asList; 051import static java.util.Arrays.stream; 052import static java.util.Objects.requireNonNull; 053 054/** 055 * Defines a web service. 056 * <br> 057 * <br> 058 * The classes implementing this extension point must be declared by {@link org.sonar.api.Plugin}. 059 * <br> 060 * <h3>How to use</h3> 061 * <pre> 062 * public class HelloWs implements WebService { 063 * {@literal @}Override 064 * public void define(Context context) { 065 * NewController controller = context.createController("api/hello"); 066 * controller.setDescription("Web service example"); 067 * 068 * // create the URL /api/hello/show 069 * controller.createAction("show") 070 * .setDescription("Entry point") 071 * .setHandler(new RequestHandler() { 072 * {@literal @}Override 073 * public void handle(Request request, Response response) { 074 * // read request parameters and generates response output 075 * response.newJsonWriter() 076 * .beginObject() 077 * .prop("hello", request.mandatoryParam("key")) 078 * .endObject() 079 * .close(); 080 * } 081 * }) 082 * .createParam("key").setDescription("Example key").setRequired(true); 083 * 084 * // important to apply changes 085 * controller.done(); 086 * } 087 * } 088 * </pre> 089 * <p> 090 * Since version 5.5, a web service can call another web service to get some data. See {@link Request#localConnector()} 091 * provided by {@link RequestHandler#handle(Request, Response)}. 092 * 093 * @since 4.2 094 */ 095@ServerSide 096@ExtensionPoint 097public interface WebService extends Definable<WebService.Context> { 098 099 class Context { 100 private final Map<String, Controller> controllers = new HashMap<>(); 101 102 /** 103 * Create a new controller. 104 * <br> 105 * Structure of request URL is <code>http://<server>/<controller path>/<action path>?<parameters></code>. 106 * 107 * @param path the controller path must not start or end with "/". It is recommended to start with "api/" 108 * and to use lower-case format with underscores, for example "api/coding_rules". Usual actions 109 * are "search", "list", "show", "create" and "delete". 110 * the plural form is recommended - ex: api/projects 111 */ 112 public NewController createController(String path) { 113 return new NewController(this, path); 114 } 115 116 private void register(NewController newController) { 117 if (controllers.containsKey(newController.path)) { 118 throw new IllegalStateException( 119 format("The web service '%s' is defined multiple times", newController.path)); 120 } 121 controllers.put(newController.path, new Controller(newController)); 122 } 123 124 @CheckForNull 125 public Controller controller(String key) { 126 return controllers.get(key); 127 } 128 129 public List<Controller> controllers() { 130 return Collections.unmodifiableList(new ArrayList<>(controllers.values())); 131 } 132 } 133 134 class NewController { 135 private final Context context; 136 private final String path; 137 private String description; 138 private String since; 139 private final Map<String, NewAction> actions = new HashMap<>(); 140 141 private NewController(Context context, String path) { 142 if (StringUtils.isBlank(path)) { 143 throw new IllegalArgumentException("WS controller path must not be empty"); 144 } 145 if (StringUtils.startsWith(path, "/") || StringUtils.endsWith(path, "/")) { 146 throw new IllegalArgumentException("WS controller path must not start or end with slash: " + path); 147 } 148 this.context = context; 149 this.path = path; 150 } 151 152 /** 153 * Important - this method must be called in order to apply changes and make the 154 * controller available in {@link org.sonar.api.server.ws.WebService.Context#controllers()} 155 */ 156 public void done() { 157 context.register(this); 158 } 159 160 /** 161 * Optional description (accept HTML) 162 */ 163 public NewController setDescription(@Nullable String s) { 164 this.description = s; 165 return this; 166 } 167 168 /** 169 * Optional version when the controller was created 170 */ 171 public NewController setSince(@Nullable String s) { 172 this.since = s; 173 return this; 174 } 175 176 public NewAction createAction(String actionKey) { 177 if (actions.containsKey(actionKey)) { 178 throw new IllegalStateException( 179 format("The action '%s' is defined multiple times in the web service '%s'", actionKey, path)); 180 } 181 NewAction action = new NewAction(actionKey); 182 actions.put(actionKey, action); 183 return action; 184 } 185 } 186 187 @Immutable 188 class Controller { 189 private final String path; 190 private final String description; 191 private final String since; 192 private final Map<String, Action> actions; 193 194 private Controller(NewController newController) { 195 checkState(!newController.actions.isEmpty(), "At least one action must be declared in the web service '%s'", newController.path); 196 this.path = newController.path; 197 this.description = newController.description; 198 this.since = newController.since; 199 Map<String, Action> mapBuilder = new HashMap<>(); 200 for (NewAction newAction : newController.actions.values()) { 201 mapBuilder.put(newAction.key, new Action(this, newAction)); 202 } 203 this.actions = Collections.unmodifiableMap(mapBuilder); 204 } 205 206 public String path() { 207 return path; 208 } 209 210 @CheckForNull 211 public String description() { 212 return description; 213 } 214 215 @CheckForNull 216 public String since() { 217 return since; 218 } 219 220 @CheckForNull 221 public Action action(String actionKey) { 222 return actions.get(actionKey); 223 } 224 225 public Collection<Action> actions() { 226 return actions.values(); 227 } 228 229 /** 230 * Returns true if all the actions are for internal use 231 * 232 * @see org.sonar.api.server.ws.WebService.Action#isInternal() 233 * @since 4.3 234 */ 235 public boolean isInternal() { 236 for (Action action : actions()) { 237 if (!action.isInternal()) { 238 return false; 239 } 240 } 241 return true; 242 } 243 } 244 245 class NewAction { 246 private final String key; 247 private String deprecatedKey; 248 private String description; 249 private String since; 250 private String deprecatedSince; 251 private boolean post = false; 252 private boolean isInternal = false; 253 private RequestHandler handler; 254 private Map<String, NewParam> newParams = new HashMap<>(); 255 private URL responseExample = null; 256 private List<Change> changelog = new ArrayList<>(); 257 258 private NewAction(String key) { 259 this.key = key; 260 } 261 262 public NewAction setDeprecatedKey(@Nullable String s) { 263 this.deprecatedKey = s; 264 return this; 265 } 266 267 /** 268 * Used in Orchestrator 269 */ 270 public NewAction setDescription(@Nullable String description) { 271 this.description = description; 272 return this; 273 } 274 275 /** 276 * @since 5.6 277 */ 278 public NewAction setDescription(@Nullable String description, Object... descriptionArgument) { 279 this.description = description == null ? null : String.format(description, descriptionArgument); 280 return this; 281 } 282 283 public NewAction setSince(@Nullable String s) { 284 this.since = s; 285 return this; 286 } 287 288 /** 289 * @since 5.3 290 */ 291 public NewAction setDeprecatedSince(@Nullable String deprecatedSince) { 292 this.deprecatedSince = deprecatedSince; 293 return this; 294 } 295 296 public NewAction setPost(boolean b) { 297 this.post = b; 298 return this; 299 } 300 301 /** 302 * Internal actions are not displayed by default in the web api documentation. They are 303 * displayed only when the check-box "Show Internal API" is selected. By default 304 * an action is not internal. 305 */ 306 public NewAction setInternal(boolean b) { 307 this.isInternal = b; 308 return this; 309 } 310 311 public NewAction setHandler(RequestHandler h) { 312 this.handler = h; 313 return this; 314 } 315 316 /** 317 * Link to the document containing an example of response. Content must be UTF-8 encoded. 318 * <br> 319 * Example: 320 * <pre> 321 * newAction.setResponseExample(getClass().getResource("/org/sonar/my-ws-response-example.json")); 322 * </pre> 323 * 324 * @since 4.4 325 */ 326 public NewAction setResponseExample(@Nullable URL url) { 327 this.responseExample = url; 328 return this; 329 } 330 331 /** 332 * List of changes made to the contract or valuable insight. Example: changes to the response format. 333 * 334 * @since 6.4 335 */ 336 public NewAction setChangelog(Change... changes) { 337 this.changelog = stream(requireNonNull(changes)) 338 .filter(Objects::nonNull) 339 .collect(Collectors.toList()); 340 341 return this; 342 } 343 344 public NewParam createParam(String paramKey) { 345 checkState(!newParams.containsKey(paramKey), "The parameter '%s' is defined multiple times in the action '%s'", paramKey, key); 346 NewParam newParam = new NewParam(paramKey); 347 newParams.put(paramKey, newParam); 348 return newParam; 349 } 350 351 /** 352 * Add predefined parameters related to pagination of results. 353 */ 354 public NewAction addPagingParams(int defaultPageSize) { 355 createParam(Param.PAGE) 356 .setDescription("1-based page number") 357 .setExampleValue("42") 358 .setDeprecatedKey("pageIndex", "5.2") 359 .setDefaultValue("1"); 360 361 createParam(Param.PAGE_SIZE) 362 .setDescription("Page size. Must be greater than 0.") 363 .setExampleValue("20") 364 .setDeprecatedKey("pageSize", "5.2") 365 .setDefaultValue(String.valueOf(defaultPageSize)); 366 return this; 367 } 368 369 /** 370 * Add predefined parameters related to pagination of results with a maximum page size. 371 * Note the maximum is a documentation only feature. It does not check anything. 372 */ 373 public NewAction addPagingParams(int defaultPageSize, int maxPageSize) { 374 createPageParam(); 375 createPageSize(defaultPageSize, maxPageSize); 376 return this; 377 } 378 379 public NewParam createPageParam() { 380 return createParam(Param.PAGE) 381 .setDescription("1-based page number") 382 .setExampleValue("42") 383 .setDeprecatedKey("pageIndex", "5.2") 384 .setDefaultValue("1"); 385 } 386 387 public NewParam createPageSize(int defaultPageSize, int maxPageSize) { 388 return createParam(Param.PAGE_SIZE) 389 .setDeprecatedKey("pageSize", "5.2") 390 .setDefaultValue(String.valueOf(defaultPageSize)) 391 .setMaximumValue(maxPageSize) 392 .setDescription("Page size. Must be greater than 0 and less than " + maxPageSize) 393 .setExampleValue("20"); 394 } 395 396 /** 397 * Creates the parameter {@link org.sonar.api.server.ws.WebService.Param#FIELDS}, which is 398 * used to restrict the number of fields returned in JSON response. 399 */ 400 public NewAction addFieldsParam(Collection<?> possibleValues) { 401 createFieldsParam(possibleValues); 402 return this; 403 } 404 405 public NewParam createFieldsParam(Collection<?> possibleValues) { 406 return createParam(Param.FIELDS) 407 .setDescription("Comma-separated list of the fields to be returned in response. All the fields are returned by default.") 408 .setPossibleValues(possibleValues); 409 } 410 411 /** 412 * Creates the parameter {@link org.sonar.api.server.ws.WebService.Param#TEXT_QUERY}, which is 413 * used to search for a subset of fields containing the supplied string. 414 * <p> 415 * The fields must be in the <strong>plural</strong> form (ex: "names", "keys"). 416 * </p> 417 */ 418 public NewAction addSearchQuery(String exampleValue, String... pluralFields) { 419 createSearchQuery(exampleValue, pluralFields); 420 return this; 421 } 422 423 /** 424 * Creates the parameter {@link org.sonar.api.server.ws.WebService.Param#TEXT_QUERY}, which is 425 * used to search for a subset of fields containing the supplied string. 426 * <p> 427 * The fields must be in the <strong>plural</strong> form (ex: "names", "keys"). 428 * </p> 429 */ 430 public NewParam createSearchQuery(String exampleValue, String... pluralFields) { 431 String actionDescription = format("Limit search to %s that contain the supplied string.", String.join(" or ", pluralFields)); 432 433 return createParam(Param.TEXT_QUERY) 434 .setDescription(actionDescription) 435 .setExampleValue(exampleValue); 436 } 437 438 /** 439 * Add predefined parameters related to sorting of results. 440 */ 441 public <V> NewAction addSortParams(Collection<V> possibleValues, @Nullable V defaultValue, boolean defaultAscending) { 442 createSortParams(possibleValues, defaultValue, defaultAscending); 443 return this; 444 } 445 446 /** 447 * Add predefined parameters related to sorting of results. 448 */ 449 public <V> NewParam createSortParams(Collection<V> possibleValues, @Nullable V defaultValue, boolean defaultAscending) { 450 createParam(Param.ASCENDING) 451 .setDescription("Ascending sort") 452 .setBooleanPossibleValues() 453 .setDefaultValue(defaultAscending); 454 455 return createParam(Param.SORT) 456 .setDescription("Sort field") 457 .setDeprecatedKey("sort", "5.4") 458 .setDefaultValue(defaultValue) 459 .setPossibleValues(possibleValues); 460 } 461 462 /** 463 * Add 'selected=(selected|deselected|all)' for select-list oriented WS. 464 */ 465 public NewAction addSelectionModeParam() { 466 createParam(Param.SELECTED) 467 .setDescription("Depending on the value, show only selected items (selected=selected), deselected items (selected=deselected), " + 468 "or all items with their selection status (selected=all).") 469 .setDefaultValue(SelectionMode.SELECTED.value()) 470 .setPossibleValues(SelectionMode.possibleValues()); 471 return this; 472 } 473 } 474 475 @Immutable 476 class Action { 477 private static final Logger LOGGER = Loggers.get(Action.class); 478 479 private final String key; 480 private final String deprecatedKey; 481 private final String path; 482 private final String description; 483 private final String since; 484 private final String deprecatedSince; 485 private final boolean post; 486 private final boolean isInternal; 487 private final RequestHandler handler; 488 private final Map<String, Param> params; 489 private final URL responseExample; 490 private final List<Change> changelog; 491 492 private Action(Controller controller, NewAction newAction) { 493 this.key = newAction.key; 494 this.deprecatedKey = newAction.deprecatedKey; 495 this.path = format("%s/%s", controller.path(), key); 496 this.description = newAction.description; 497 this.since = newAction.since; 498 this.deprecatedSince = newAction.deprecatedSince; 499 this.post = newAction.post; 500 this.isInternal = newAction.isInternal; 501 this.responseExample = newAction.responseExample; 502 this.handler = newAction.handler; 503 this.changelog = newAction.changelog; 504 505 checkState(this.handler != null, "RequestHandler is not set on action %s", path); 506 logWarningIf(isNullOrEmpty(this.description), "Description is not set on action " + path); 507 logWarningIf(isNullOrEmpty(this.since), "Since is not set on action " + path); 508 logWarningIf(!this.post && this.responseExample == null, "The response example is not set on action " + path); 509 510 Map<String, Param> paramsBuilder = new HashMap<>(); 511 for (NewParam newParam : newAction.newParams.values()) { 512 paramsBuilder.put(newParam.key, new Param(this, newParam)); 513 } 514 this.params = Collections.unmodifiableMap(paramsBuilder); 515 } 516 517 private static void logWarningIf(boolean condition, String message) { 518 if (condition) { 519 LOGGER.warn(message); 520 } 521 } 522 523 public String key() { 524 return key; 525 } 526 527 public String deprecatedKey() { 528 return deprecatedKey; 529 } 530 531 public String path() { 532 return path; 533 } 534 535 @CheckForNull 536 public String description() { 537 return description; 538 } 539 540 /** 541 * Set if different than controller. 542 */ 543 @CheckForNull 544 public String since() { 545 return since; 546 } 547 548 @CheckForNull 549 public String deprecatedSince() { 550 return deprecatedSince; 551 } 552 553 public boolean isPost() { 554 return post; 555 } 556 557 /** 558 * @see NewAction#setChangelog(Change...) 559 * @since 6.4 560 */ 561 public List<Change> changelog() { 562 return changelog; 563 } 564 565 /** 566 * @see NewAction#setInternal(boolean) 567 */ 568 public boolean isInternal() { 569 return isInternal; 570 } 571 572 public RequestHandler handler() { 573 return handler; 574 } 575 576 /** 577 * @see org.sonar.api.server.ws.WebService.NewAction#setResponseExample(java.net.URL) 578 */ 579 @CheckForNull 580 public URL responseExample() { 581 return responseExample; 582 } 583 584 /** 585 * @see org.sonar.api.server.ws.WebService.NewAction#setResponseExample(java.net.URL) 586 */ 587 @CheckForNull 588 public String responseExampleAsString() { 589 try { 590 if (responseExample != null) { 591 return StringUtils.trim(IOUtils.toString(responseExample, StandardCharsets.UTF_8)); 592 } 593 return null; 594 } catch (IOException e) { 595 throw new IllegalStateException("Fail to load " + responseExample, e); 596 } 597 } 598 599 /** 600 * @see org.sonar.api.server.ws.WebService.NewAction#setResponseExample(java.net.URL) 601 */ 602 @CheckForNull 603 public String responseExampleFormat() { 604 if (responseExample != null) { 605 return StringUtils.lowerCase(FilenameUtils.getExtension(responseExample.getFile())); 606 } 607 return null; 608 } 609 610 @CheckForNull 611 public Param param(String key) { 612 return params.get(key); 613 } 614 615 public Collection<Param> params() { 616 return params.values(); 617 } 618 619 @Override 620 public String toString() { 621 return path; 622 } 623 } 624 625 class NewParam { 626 private String key; 627 private String since; 628 private String deprecatedSince; 629 private String deprecatedKey; 630 private String deprecatedKeySince; 631 private String description; 632 private String exampleValue; 633 private String defaultValue; 634 private boolean required = false; 635 private boolean internal = false; 636 private Set<String> possibleValues = null; 637 private Integer maxValuesAllowed; 638 private Integer maximumLength; 639 private Integer minimumLength; 640 private Integer maximumValue; 641 642 private NewParam(String key) { 643 this.key = key; 644 } 645 646 /** 647 * @since 5.3 648 * @see Param#since() 649 */ 650 public NewParam setSince(@Nullable String since) { 651 this.since = since; 652 return this; 653 } 654 655 /** 656 * @since 5.3 657 */ 658 public NewParam setDeprecatedSince(@Nullable String deprecatedSince) { 659 this.deprecatedSince = deprecatedSince; 660 return this; 661 } 662 663 /** 664 * @see #setDeprecatedKey(String, String) 665 * @since 5.0 666 * @deprecated since 6.4 667 */ 668 @Deprecated 669 public NewParam setDeprecatedKey(@Nullable String s) { 670 this.deprecatedKey = s; 671 return this; 672 } 673 674 /** 675 * @param deprecatedSince Version when the old key was replaced/deprecated. Ex: 5.6 676 * @since 6.4 677 * @see Param#deprecatedKey() 678 */ 679 public NewParam setDeprecatedKey(@Nullable String key, @Nullable String deprecatedSince) { 680 this.deprecatedKey = key; 681 this.deprecatedKeySince = deprecatedSince; 682 return this; 683 } 684 685 public NewParam setDescription(@Nullable String description) { 686 this.description = description; 687 return this; 688 } 689 690 /** 691 * @since 5.6 692 * @see Param#description() 693 */ 694 public NewParam setDescription(@Nullable String description, Object... descriptionArgument) { 695 this.description = description == null ? null : String.format(description, descriptionArgument); 696 return this; 697 } 698 699 /** 700 * Is the parameter required or optional ? Default value is false (optional). 701 * 702 * @since 4.4 703 * @see Param#isRequired() 704 */ 705 public NewParam setRequired(boolean b) { 706 this.required = b; 707 return this; 708 } 709 710 /** 711 * Internal parameters are not displayed by default in the web api documentation. They are 712 * displayed only when the check-box "Show Internal API" is selected. By default 713 * a parameter is not internal. 714 * 715 * @since 6.2 716 * @see Param#isInternal() 717 */ 718 public NewParam setInternal(boolean b) { 719 this.internal = b; 720 return this; 721 } 722 723 /** 724 * @since 4.4 725 * @see Param#exampleValue() 726 */ 727 public NewParam setExampleValue(@Nullable Object s) { 728 this.exampleValue = (s != null) ? s.toString() : null; 729 return this; 730 } 731 732 /** 733 * Exhaustive list of possible values when it makes sense, for example 734 * list of severities. 735 * 736 * @since 4.4 737 * @see Param#possibleValues() 738 */ 739 public NewParam setPossibleValues(@Nullable Object... values) { 740 return setPossibleValues(values == null ? Collections.emptyList() : asList(values)); 741 } 742 743 /** 744 * Shortcut for {@code setPossibleValues("true", "false", "yes", "no")} 745 * @since 4.4 746 */ 747 public NewParam setBooleanPossibleValues() { 748 return setPossibleValues("true", "false", "yes", "no"); 749 } 750 751 /** 752 * Exhaustive list of possible values when it makes sense, for example 753 * list of severities. 754 * 755 * @since 4.4 756 * @see Param#possibleValues() 757 */ 758 public NewParam setPossibleValues(@Nullable Collection<?> values) { 759 if (values == null || values.isEmpty()) { 760 this.possibleValues = null; 761 } else { 762 this.possibleValues = new LinkedHashSet<>(); 763 for (Object value : values) { 764 this.possibleValues.add(value.toString()); 765 } 766 } 767 return this; 768 } 769 770 /** 771 * @since 4.4 772 * @see Param#defaultValue() 773 */ 774 public NewParam setDefaultValue(@Nullable Object o) { 775 this.defaultValue = (o != null) ? o.toString() : null; 776 return this; 777 } 778 779 /** 780 * @since 6.4 781 * @see Param#maxValuesAllowed() 782 */ 783 public NewParam setMaxValuesAllowed(@Nullable Integer maxValuesAllowed) { 784 this.maxValuesAllowed = maxValuesAllowed; 785 return this; 786 } 787 788 /** 789 * @since 7.0 790 * @see Param#maximumLength() 791 */ 792 public NewParam setMaximumLength(@Nullable Integer maximumLength) { 793 this.maximumLength = maximumLength; 794 return this; 795 } 796 797 /** 798 * @since 7.0 799 * @see Param#minimumLength() 800 */ 801 public NewParam setMinimumLength(@Nullable Integer minimumLength) { 802 this.minimumLength = minimumLength; 803 return this; 804 } 805 806 /** 807 * @since 7.0 808 * @see Param#maximumValue() 809 */ 810 public NewParam setMaximumValue(@Nullable Integer maximumValue) { 811 this.maximumValue = maximumValue; 812 return this; 813 } 814 815 @Override 816 public String toString() { 817 return key; 818 } 819 } 820 821 enum SelectionMode { 822 SELECTED("selected"), DESELECTED("deselected"), ALL("all"); 823 824 private final String paramValue; 825 826 private static final Map<String, SelectionMode> BY_VALUE = stream(values()) 827 .collect(Collectors.toMap(v -> v.paramValue, v -> v)); 828 829 SelectionMode(String paramValue) { 830 this.paramValue = paramValue; 831 } 832 833 public String value() { 834 return paramValue; 835 } 836 837 public static SelectionMode fromParam(String paramValue) { 838 checkArgument(BY_VALUE.containsKey(paramValue)); 839 return BY_VALUE.get(paramValue); 840 } 841 842 public static Collection<String> possibleValues() { 843 return BY_VALUE.keySet(); 844 } 845 } 846 847 @Immutable 848 class Param { 849 public static final String TEXT_QUERY = "q"; 850 public static final String PAGE = "p"; 851 public static final String PAGE_SIZE = "ps"; 852 public static final String FIELDS = "f"; 853 public static final String SORT = "s"; 854 public static final String ASCENDING = "asc"; 855 public static final String FACETS = "facets"; 856 public static final String SELECTED = "selected"; 857 858 private final String key; 859 private final String since; 860 private final String deprecatedSince; 861 private final String deprecatedKey; 862 private final String deprecatedKeySince; 863 private final String description; 864 private final String exampleValue; 865 private final String defaultValue; 866 private final boolean required; 867 private final boolean internal; 868 private final Set<String> possibleValues; 869 private final Integer maximumLength; 870 private final Integer minimumLength; 871 private final Integer maximumValue; 872 private final Integer maxValuesAllowed; 873 874 protected Param(Action action, NewParam newParam) { 875 this.key = newParam.key; 876 this.since = newParam.since; 877 this.deprecatedSince = newParam.deprecatedSince; 878 this.deprecatedKey = newParam.deprecatedKey; 879 this.deprecatedKeySince = newParam.deprecatedKeySince; 880 this.description = newParam.description; 881 this.exampleValue = newParam.exampleValue; 882 this.defaultValue = newParam.defaultValue; 883 this.required = newParam.required; 884 this.internal = newParam.internal; 885 this.possibleValues = newParam.possibleValues; 886 this.maxValuesAllowed = newParam.maxValuesAllowed; 887 this.maximumLength = newParam.maximumLength; 888 this.minimumLength = newParam.minimumLength; 889 this.maximumValue = newParam.maximumValue; 890 checkArgument(!required || defaultValue == null, "Default value must not be set on parameter '%s?%s' as it's marked as required", action, key); 891 } 892 893 public String key() { 894 return key; 895 } 896 897 /** 898 * @since 5.3 899 */ 900 @CheckForNull 901 public String since() { 902 return since; 903 } 904 905 /** 906 * @since 5.3 907 */ 908 @CheckForNull 909 public String deprecatedSince() { 910 return deprecatedSince; 911 } 912 913 /** 914 * @since 5.0 915 */ 916 @CheckForNull 917 public String deprecatedKey() { 918 return deprecatedKey; 919 } 920 921 /** 922 * @since 6.4 923 */ 924 @CheckForNull 925 public String deprecatedKeySince() { 926 return deprecatedKeySince; 927 } 928 929 @CheckForNull 930 public String description() { 931 return description; 932 } 933 934 /** 935 * @since 4.4 936 */ 937 @CheckForNull 938 public String exampleValue() { 939 return exampleValue; 940 } 941 942 /** 943 * Is the parameter required or optional ? 944 * 945 * @since 4.4 946 */ 947 public boolean isRequired() { 948 return required; 949 } 950 951 /** 952 * Is the parameter internal ? 953 * 954 * @see NewParam#setInternal(boolean) 955 * @since 6.2 956 */ 957 public boolean isInternal() { 958 return internal; 959 } 960 961 /** 962 * @since 4.4 963 */ 964 @CheckForNull 965 public Set<String> possibleValues() { 966 return possibleValues; 967 } 968 969 /** 970 * @since 4.4 971 */ 972 @CheckForNull 973 public String defaultValue() { 974 return defaultValue; 975 } 976 977 /** 978 * Specify the maximum number of values allowed when using {@link Request#multiParam(String)} 979 * 980 * @since 6.4 981 */ 982 public Integer maxValuesAllowed() { 983 return maxValuesAllowed; 984 } 985 986 /** 987 * Specify the maximum length of the value used in this parameter 988 * 989 * @since 7.0 990 */ 991 @CheckForNull 992 public Integer maximumLength() { 993 return maximumLength; 994 } 995 996 /** 997 * Specify the minimum length of the value used in this parameter 998 * 999 * @since 7.0 1000 */ 1001 @CheckForNull 1002 public Integer minimumLength() { 1003 return minimumLength; 1004 } 1005 1006 /** 1007 * Specify the maximum value of the numeric variable used in this parameter 1008 * 1009 * @since 7.0 1010 */ 1011 @CheckForNull 1012 public Integer maximumValue() { 1013 return maximumValue; 1014 } 1015 1016 @Override 1017 public String toString() { 1018 return key; 1019 } 1020 } 1021 1022 /** 1023 * Executed once at server startup. 1024 */ 1025 @Override 1026 void define(Context context); 1027 1028}