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