001/* 002 * SonarQube 003 * Copyright (C) 2009-2017 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 .setDescription("Page size. Must be greater than 0 and less than " + maxPageSize) 390 .setExampleValue("20") 391 .setDeprecatedKey("pageSize", "5.2") 392 .setDefaultValue(String.valueOf(defaultPageSize)); 393 } 394 395 /** 396 * Creates the parameter {@link org.sonar.api.server.ws.WebService.Param#FIELDS}, which is 397 * used to restrict the number of fields returned in JSON response. 398 */ 399 public NewAction addFieldsParam(Collection<?> possibleValues) { 400 createFieldsParam(possibleValues); 401 return this; 402 } 403 404 public NewParam createFieldsParam(Collection<?> possibleValues) { 405 return createParam(Param.FIELDS) 406 .setDescription("Comma-separated list of the fields to be returned in response. All the fields are returned by default.") 407 .setPossibleValues(possibleValues); 408 } 409 410 /** 411 * Creates the parameter {@link org.sonar.api.server.ws.WebService.Param#TEXT_QUERY}, which is 412 * used to search for a subset of fields containing the supplied string. 413 * <p> 414 * The fields must be in the <strong>plural</strong> form (ex: "names", "keys"). 415 * </p> 416 */ 417 public NewAction addSearchQuery(String exampleValue, String... pluralFields) { 418 createSearchQuery(exampleValue, pluralFields); 419 return this; 420 } 421 422 /** 423 * Creates the parameter {@link org.sonar.api.server.ws.WebService.Param#TEXT_QUERY}, which is 424 * used to search for a subset of fields containing the supplied string. 425 * <p> 426 * The fields must be in the <strong>plural</strong> form (ex: "names", "keys"). 427 * </p> 428 */ 429 public NewParam createSearchQuery(String exampleValue, String... pluralFields) { 430 String actionDescription = format("Limit search to %s that contain the supplied string.", String.join(" or ", pluralFields)); 431 432 return createParam(Param.TEXT_QUERY) 433 .setDescription(actionDescription) 434 .setExampleValue(exampleValue); 435 } 436 437 /** 438 * Add predefined parameters related to sorting of results. 439 */ 440 public <V> NewAction addSortParams(Collection<V> possibleValues, @Nullable V defaultValue, boolean defaultAscending) { 441 createSortParams(possibleValues, defaultValue, defaultAscending); 442 return this; 443 } 444 445 /** 446 * Add predefined parameters related to sorting of results. 447 */ 448 public <V> NewParam createSortParams(Collection<V> possibleValues, @Nullable V defaultValue, boolean defaultAscending) { 449 createParam(Param.ASCENDING) 450 .setDescription("Ascending sort") 451 .setBooleanPossibleValues() 452 .setDefaultValue(defaultAscending); 453 454 return createParam(Param.SORT) 455 .setDescription("Sort field") 456 .setDeprecatedKey("sort", "5.4") 457 .setDefaultValue(defaultValue) 458 .setPossibleValues(possibleValues); 459 } 460 461 /** 462 * Add 'selected=(selected|deselected|all)' for select-list oriented WS. 463 */ 464 public NewAction addSelectionModeParam() { 465 createParam(Param.SELECTED) 466 .setDescription("Depending on the value, show only selected items (selected=selected), deselected items (selected=deselected), " + 467 "or all items with their selection status (selected=all).") 468 .setDefaultValue(SelectionMode.SELECTED.value()) 469 .setPossibleValues(SelectionMode.possibleValues()); 470 return this; 471 } 472 } 473 474 @Immutable 475 class Action { 476 private static final Logger LOGGER = Loggers.get(Action.class); 477 478 private final String key; 479 private final String deprecatedKey; 480 private final String path; 481 private final String description; 482 private final String since; 483 private final String deprecatedSince; 484 private final boolean post; 485 private final boolean isInternal; 486 private final RequestHandler handler; 487 private final Map<String, Param> params; 488 private final URL responseExample; 489 private final List<Change> changelog; 490 491 private Action(Controller controller, NewAction newAction) { 492 this.key = newAction.key; 493 this.deprecatedKey = newAction.deprecatedKey; 494 this.path = format("%s/%s", controller.path(), key); 495 this.description = newAction.description; 496 this.since = newAction.since; 497 this.deprecatedSince = newAction.deprecatedSince; 498 this.post = newAction.post; 499 this.isInternal = newAction.isInternal; 500 this.responseExample = newAction.responseExample; 501 this.handler = newAction.handler; 502 this.changelog = newAction.changelog; 503 504 checkState(this.handler != null, "RequestHandler is not set on action %s", path); 505 logWarningIf(isNullOrEmpty(this.description), "Description is not set on action " + path); 506 logWarningIf(isNullOrEmpty(this.since), "Since is not set on action " + path); 507 logWarningIf(!this.post && this.responseExample == null, "The response example is not set on action " + path); 508 509 Map<String, Param> paramsBuilder = new HashMap<>(); 510 for (NewParam newParam : newAction.newParams.values()) { 511 paramsBuilder.put(newParam.key, new Param(this, newParam)); 512 } 513 this.params = Collections.unmodifiableMap(paramsBuilder); 514 } 515 516 private static void logWarningIf(boolean condition, String message) { 517 if (condition) { 518 LOGGER.warn(message); 519 } 520 } 521 522 public String key() { 523 return key; 524 } 525 526 public String deprecatedKey() { 527 return deprecatedKey; 528 } 529 530 public String path() { 531 return path; 532 } 533 534 @CheckForNull 535 public String description() { 536 return description; 537 } 538 539 /** 540 * Set if different than controller. 541 */ 542 @CheckForNull 543 public String since() { 544 return since; 545 } 546 547 @CheckForNull 548 public String deprecatedSince() { 549 return deprecatedSince; 550 } 551 552 public boolean isPost() { 553 return post; 554 } 555 556 /** 557 * @see NewAction#setChangelog(Change...) 558 * @since 6.4 559 */ 560 public List<Change> changelog() { 561 return changelog; 562 } 563 564 /** 565 * @see NewAction#setInternal(boolean) 566 */ 567 public boolean isInternal() { 568 return isInternal; 569 } 570 571 public RequestHandler handler() { 572 return handler; 573 } 574 575 /** 576 * @see org.sonar.api.server.ws.WebService.NewAction#setResponseExample(java.net.URL) 577 */ 578 @CheckForNull 579 public URL responseExample() { 580 return responseExample; 581 } 582 583 /** 584 * @see org.sonar.api.server.ws.WebService.NewAction#setResponseExample(java.net.URL) 585 */ 586 @CheckForNull 587 public String responseExampleAsString() { 588 try { 589 if (responseExample != null) { 590 return StringUtils.trim(IOUtils.toString(responseExample, StandardCharsets.UTF_8)); 591 } 592 return null; 593 } catch (IOException e) { 594 throw new IllegalStateException("Fail to load " + responseExample, e); 595 } 596 } 597 598 /** 599 * @see org.sonar.api.server.ws.WebService.NewAction#setResponseExample(java.net.URL) 600 */ 601 @CheckForNull 602 public String responseExampleFormat() { 603 if (responseExample != null) { 604 return StringUtils.lowerCase(FilenameUtils.getExtension(responseExample.getFile())); 605 } 606 return null; 607 } 608 609 @CheckForNull 610 public Param param(String key) { 611 return params.get(key); 612 } 613 614 public Collection<Param> params() { 615 return params.values(); 616 } 617 618 @Override 619 public String toString() { 620 return path; 621 } 622 } 623 624 class NewParam { 625 private String key; 626 private String since; 627 private String deprecatedSince; 628 private String deprecatedKey; 629 private String deprecatedKeySince; 630 private String description; 631 private String exampleValue; 632 private String defaultValue; 633 private boolean required = false; 634 private boolean internal = false; 635 private Set<String> possibleValues = null; 636 private Integer maxValuesAllowed = null; 637 638 private NewParam(String key) { 639 this.key = key; 640 } 641 642 /** 643 * @since 5.3 644 */ 645 public NewParam setSince(@Nullable String since) { 646 this.since = since; 647 return this; 648 } 649 650 /** 651 * @since 5.3 652 */ 653 public NewParam setDeprecatedSince(@Nullable String deprecatedSince) { 654 this.deprecatedSince = deprecatedSince; 655 return this; 656 } 657 658 /** 659 * @see #setDeprecatedKey(String, String) 660 * @since 5.0 661 * @deprecated since 6.4 662 */ 663 @Deprecated 664 public NewParam setDeprecatedKey(@Nullable String s) { 665 this.deprecatedKey = s; 666 return this; 667 } 668 669 /** 670 * @param deprecatedSince Version when the old key was replaced/deprecated. Ex: 5.6 671 * @since 6.4 672 */ 673 public NewParam setDeprecatedKey(@Nullable String key, @Nullable String deprecatedSince) { 674 this.deprecatedKey = key; 675 this.deprecatedKeySince = deprecatedSince; 676 return this; 677 } 678 679 public NewParam setDescription(@Nullable String description) { 680 this.description = description; 681 return this; 682 } 683 684 /** 685 * @since 5.6 686 */ 687 public NewParam setDescription(@Nullable String description, Object... descriptionArgument) { 688 this.description = description == null ? null : String.format(description, descriptionArgument); 689 return this; 690 } 691 692 /** 693 * Is the parameter required or optional ? Default value is false (optional). 694 * 695 * @since 4.4 696 */ 697 public NewParam setRequired(boolean b) { 698 this.required = b; 699 return this; 700 } 701 702 /** 703 * Internal parameters are not displayed by default in the web api documentation. They are 704 * displayed only when the check-box "Show Internal API" is selected. By default 705 * a parameter is not internal. 706 * 707 * @since 6.2 708 */ 709 public NewParam setInternal(boolean b) { 710 this.internal = b; 711 return this; 712 } 713 714 /** 715 * @since 4.4 716 */ 717 public NewParam setExampleValue(@Nullable Object s) { 718 this.exampleValue = (s != null) ? s.toString() : null; 719 return this; 720 } 721 722 /** 723 * Exhaustive list of possible values when it makes sense, for example 724 * list of severities. 725 * 726 * @since 4.4 727 */ 728 public NewParam setPossibleValues(@Nullable Object... values) { 729 return setPossibleValues(values == null ? Collections.emptyList() : asList(values)); 730 } 731 732 /** 733 * @since 4.4 734 */ 735 public NewParam setBooleanPossibleValues() { 736 return setPossibleValues("true", "false", "yes", "no"); 737 } 738 739 /** 740 * Exhaustive list of possible values when it makes sense, for example 741 * list of severities. 742 * 743 * @since 4.4 744 */ 745 public NewParam setPossibleValues(@Nullable Collection<?> values) { 746 if (values == null || values.isEmpty()) { 747 this.possibleValues = null; 748 } else { 749 this.possibleValues = new LinkedHashSet<>(); 750 for (Object value : values) { 751 this.possibleValues.add(value.toString()); 752 } 753 } 754 return this; 755 } 756 757 /** 758 * @since 4.4 759 */ 760 public NewParam setDefaultValue(@Nullable Object o) { 761 this.defaultValue = (o != null) ? o.toString() : null; 762 return this; 763 } 764 765 /** 766 * @since 6.4 767 */ 768 public NewParam setMaxValuesAllowed(@Nullable Integer maxValuesAllowed) { 769 this.maxValuesAllowed = maxValuesAllowed; 770 return this; 771 } 772 773 @Override 774 public String toString() { 775 return key; 776 } 777 } 778 779 enum SelectionMode { 780 SELECTED("selected"), DESELECTED("deselected"), ALL("all"); 781 782 private final String paramValue; 783 784 private static final Map<String, SelectionMode> BY_VALUE = stream(values()) 785 .collect(Collectors.toMap(v -> v.paramValue, v -> v)); 786 787 SelectionMode(String paramValue) { 788 this.paramValue = paramValue; 789 } 790 791 public String value() { 792 return paramValue; 793 } 794 795 public static SelectionMode fromParam(String paramValue) { 796 checkArgument(BY_VALUE.containsKey(paramValue)); 797 return BY_VALUE.get(paramValue); 798 } 799 800 public static Collection<String> possibleValues() { 801 return BY_VALUE.keySet(); 802 } 803 } 804 805 @Immutable 806 class Param { 807 public static final String TEXT_QUERY = "q"; 808 public static final String PAGE = "p"; 809 public static final String PAGE_SIZE = "ps"; 810 public static final String FIELDS = "f"; 811 public static final String SORT = "s"; 812 public static final String ASCENDING = "asc"; 813 public static final String FACETS = "facets"; 814 public static final String SELECTED = "selected"; 815 816 private final String key; 817 private final String since; 818 private final String deprecatedSince; 819 private final String deprecatedKey; 820 private final String deprecatedKeySince; 821 private final String description; 822 private final String exampleValue; 823 private final String defaultValue; 824 private final boolean required; 825 private final boolean internal; 826 private final Set<String> possibleValues; 827 private final Integer maxValuesAllowed; 828 829 protected Param(Action action, NewParam newParam) { 830 this.key = newParam.key; 831 this.since = newParam.since; 832 this.deprecatedSince = newParam.deprecatedSince; 833 this.deprecatedKey = newParam.deprecatedKey; 834 this.deprecatedKeySince = newParam.deprecatedKeySince; 835 this.description = newParam.description; 836 this.exampleValue = newParam.exampleValue; 837 this.defaultValue = newParam.defaultValue; 838 this.required = newParam.required; 839 this.internal = newParam.internal; 840 this.possibleValues = newParam.possibleValues; 841 this.maxValuesAllowed = newParam.maxValuesAllowed; 842 checkArgument(!required || defaultValue == null, "Default value must not be set on parameter '%s?%s' as it's marked as required", action, key); 843 } 844 845 public String key() { 846 return key; 847 } 848 849 /** 850 * @since 5.3 851 */ 852 @CheckForNull 853 public String since() { 854 return since; 855 } 856 857 /** 858 * @since 5.3 859 */ 860 @CheckForNull 861 public String deprecatedSince() { 862 return deprecatedSince; 863 } 864 865 /** 866 * @since 5.0 867 */ 868 @CheckForNull 869 public String deprecatedKey() { 870 return deprecatedKey; 871 } 872 873 /** 874 * @since 6.4 875 */ 876 @CheckForNull 877 public String deprecatedKeySince() { 878 return deprecatedKeySince; 879 } 880 881 @CheckForNull 882 public String description() { 883 return description; 884 } 885 886 /** 887 * @since 4.4 888 */ 889 @CheckForNull 890 public String exampleValue() { 891 return exampleValue; 892 } 893 894 /** 895 * Is the parameter required or optional ? 896 * 897 * @since 4.4 898 */ 899 public boolean isRequired() { 900 return required; 901 } 902 903 /** 904 * Is the parameter internal ? 905 * 906 * @see NewParam#setInternal(boolean) 907 * @since 6.2 908 */ 909 public boolean isInternal() { 910 return internal; 911 } 912 913 /** 914 * @since 4.4 915 */ 916 @CheckForNull 917 public Set<String> possibleValues() { 918 return possibleValues; 919 } 920 921 /** 922 * @since 4.4 923 */ 924 @CheckForNull 925 public String defaultValue() { 926 return defaultValue; 927 } 928 929 /** 930 * Specify the maximum number of values allowed when using this a parameter 931 * 932 * @since 6.4 933 */ 934 public Integer maxValuesAllowed() { 935 return maxValuesAllowed; 936 } 937 938 @Override 939 public String toString() { 940 return key; 941 } 942 } 943 944 /** 945 * Executed once at server startup. 946 */ 947 @Override 948 void define(Context context); 949 950}