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